Fall 2026
  • Discord
  • Gradescope
  • Syllabus

On this page

  • The REPL
  • From expressions to functions
  • Doctests
  • Looking forward

Python

For two weeks we described things: HTML said what content is, CSS said how it should look. Neither one ever did anything. Python is different in kind. HTML and CSS are markup languages: you state what you want and the browser figures out how to produce it. Python is a procedural language: you spell out how to do something, one step at a time, and the computer does exactly that and nothing more. This is the language the rest of the course is built on, and it is the one that finally lets us automate the boring stuff for real.

It is also, for most people, much harder than HTML and CSS. If you have never programmed before, expect the next couple of weeks to feel like a wall. That feeling is normal, and it swings back and forth by the hour.

Meme titled 'the two states of every programmer': a triumphant 'I am a god' next to a dog at a keyboard captioned 'I have no idea what I'm doing.'

The way through the wall is to run code constantly and check what it actually does, rather than what you assume it does. So before anything else, let’s get a place to run code.

The REPL

The fastest way to experiment with Python is the REPL, the interactive prompt you get by running python3 in a terminal. (VS Code has a built-in terminal under Terminal → New Terminal.) It reads a line, evaluates it, prints the result, and loops (Read, Eval, Print, Loop), and its prompt is >>>. Type an expression and it shows you the value that expression computes:

>>> 17 + 25
42
>>> 2 ** 10
1024

The ** is exponentiation, so 2 ** 10 is two to the tenth. Every value has a type, and the two numeric types behave differently in a way that trips up everyone once. Division with / always gives a decimal, while // divides and throws away the remainder:

>>> 7 / 2
3.5
>>> 7 // 2
3

The 3.5 is a float (a number with a decimal point) and the 3 is an int (a whole number). You can ask any value its type with the type function:

>>> type(4)
<class 'int'>
>>> type(4.0)
<class 'float'>
>>> type("hi")
<class 'str'>
>>> type(True)
<class 'bool'>

Three more types appear there. A str (string) is text in quotes; a bool (boolean) is one of the two values True or False. Strings even do something numbers do not:

>>> "ha" * 3
'hahaha'
>>> len("automate")
8

Multiplying a string repeats it, and len reports how many characters it holds. Play in the REPL until predicting each result feels boring; that boredom is fluency arriving.

From expressions to functions

The REPL is for experiments; to keep work you write it in a .py file. And the unit of reusable work in Python is the function: a named recipe that takes some inputs and produces an output. We have already called functions (type(...) and len(...) are both functions), and now we write our own with def:

def is_even(n):
    return n % 2 == 0

Read it line by line. The def line names the function is_even and says it takes one parameter, n, a stand-in for whatever value we call it with. The indented line is the body, the steps that run when the function is called. The % operator is the modulus: it gives the remainder after division, so n % 2 is 0 exactly when n is even. The expression n % 2 == 0 compares that remainder to zero with == and evaluates to a bool, and return hands that bool back to whoever called the function.

That return is the single most important word here, because of what it is not. A return sends a value back to the surrounding program, which can then use it; print merely displays text on the screen and hands back nothing. A function that prints its answer instead of returning it looks right in the REPL and is useless everywhere else, because no other code can get at a value that was only ever painted on the screen. Compare the two:

>>> is_even(10)
True
>>> is_even(10) and is_even(7)
False

Because is_even returns its answer, we can feed it straight into more logic, here combining two calls with and. Swap the return for a print and that second line breaks, because there is no value to combine. When in doubt, return.

The remainder trick was specific to even-ness; most functions need to choose between paths or repeat a step, which is what control flow is for. An if statement runs its block only when a condition is true, with an optional else for the other case:

def absolute_value(n):
    if n < 0:
        return -n
    else:
        return n

A while loop repeats its block as long as a condition holds, and a for loop repeats once per item in a sequence. The built-in range makes the sequence of numbers most loops walk over, so range(1, n + 1) counts from 1 up to n:

def factorial(n):
    result = 1
    for i in range(1, n + 1):
        result = result * i
    return result

We start an accumulator result at 1, then walk i from 1 to n, each time multiplying result by i and storing it back. When the loop finishes, result holds 1 * 2 * ... * n, and we return it. What would go wrong if we started result at 0 instead of 1? Try it in the REPL before reading on; the answer is the kind of bug you will chase all term, and catching it yourself once is worth more than reading about it three times.

Doctests

Here is the problem those two functions raise: how do you know factorial is right? Checking it by hand in the REPL every time you change it does not scale, and it is exactly the boring, repetitive task we are here to automate. Python’s answer, and the backbone of every lab in this course, is the doctest.

A doctest is an example of your function’s behavior, written right into its documentation, in the exact form of a REPL session: the >>> prompt, the call, and the expected result on the next line:

def is_even(n):
    '''
    Return True if n is even and False if n is odd.

    >>> is_even(0)
    True
    >>> is_even(7)
    False
    >>> is_even(-8)
    True
    '''
    return n % 2 == 0

The triple-quoted text under the def line is a docstring, the function’s documentation, and those three >>> lines inside it are the tests. Each one is a promise: “called this way, the function returns this value.” Python can check every promise for you. Run the file through the doctest module from your terminal:

$ python3 -m doctest example.py

When every doctest passes, the command prints nothing at all and simply returns you to the prompt; silence means success. Add -v and it shows its work and a tally instead:

$ python3 -m doctest -v example.py
...
3 passed and 0 failed.
Test passed.

Now watch what a wrong function looks like. Suppose we slipped and wrote return n % 2 == 1. The tests turn from a promise into an accusation:

$ python3 -m doctest example.py
**********************************************************************
File "example.py", line 5, in example.is_even
Failed example:
    is_even(0)
Expected:
    True
Got:
    False
...
***Test Failed*** 3 failures.

For every broken example it prints the exact call, what you promised (Expected: True), and what the code actually did (Got: False), and it ends with a count of how many failed. All three of our examples fail here, because == 1 asks the opposite question, and the last line tallies them. Your whole job on a lab is to make that output go away. You do not write the tests (they come with the lab); you write the function until every Got matches every Expected and the command falls silent.

This is what “auto-graded, resubmit until 100%” means in practice. The lab is a .py file full of functions with doctests and empty bodies; you fill in the bodies until the doctests pass on your machine, then push the file to GitHub, where the same doctests run automatically and flip a status badge from red to green. The green badge is your grade. There is no partial credit and no ambiguity: the tests either pass or they do not, and you keep going until they do.

Looking forward

You can now write a function, choose with if, repeat with for and while, and prove the result with doctests. That is the entire toolkit for this week’s lab, Doctests: about thirty short functions, each a few minutes of work, that drill exactly these moves. Do them yourself even though an LLM could write them in seconds; the easy problems are where you build the muscle for the hard ones later, the ones an LLM cannot do.

Everything so far has computed with numbers and booleans we typed in ourselves. Next class we reach outside the program for the first time, reading real text and opening real files, and start turning Python loose on data we did not make up.