Part 1 · Foundations · Module 04

Control Flow

So far your programs have run straight from top to bottom. Real programs make decisions and repeat work: "if the password is wrong, ask again", "for every line in the file, count the words". Control flow is how you tell Python which statements to run, when, and how many times. With it, you can write genuinely useful programs.

⏱ ~2 hoursPrerequisites: M03

Boolean logic and truthiness

Every decision a program makes comes down to a condition: an expression that is either true or false. You met the Boolean values True and False and the comparison operators (==, !=, <, <=, >, >=) in M02. The logical operators combine conditions:

aba and ba or bnot a
TrueTrueTrueTrueFalse
TrueFalseFalseTrueFalse
FalseTrueFalseTrueTrue
FalseFalseFalseFalseTrue

Truthiness

Python lets you use any value as a condition. Each value is either truthy (treated as true) or falsy (treated as false). The falsy values are a short list worth memorizing:

Everything else is truthy, including "0", "False", [0], and -1. So the idiomatic way to check "is this list non-empty?" is simply if items:.

Short-circuit evaluation

and and or are lazy: they stop as soon as the answer is known. This is called short-circuiting. a and b does not even look at b if a is falsy; a or b skips b if a is truthy. They also return one of their operands, not necessarily True or False:

Remember: falsy means False, None, zero, or empty; and/or short-circuit and return an operand.

Decisions: if, elif, else

An if statement runs its block only when its condition is truthy. elif ("else if") adds more conditions, checked in order, and else catches everything that did not match. Python runs the first matching branch and skips the rest.

temperature = 18
if temperature >= 30:
    advice = "Shorts and sunscreen."
elif temperature >= 15:
    advice = "A light jacket."
elif temperature >= 0:
    advice = "A warm coat."
else:
    advice = "Stay inside!"
print(advice)

Here is that decision as a flowchart, a diagram of the paths a program can take. Diamonds are decisions, rectangles are actions.

flowchart TD
  S([start]) --> A{"temperature >= 30?"}
  A -- yes --> R1["Shorts and sunscreen"]
  A -- no --> B{"temperature >= 15?"}
  B -- yes --> R2["A light jacket"]
  B -- no --> C{"temperature >= 0?"}
  C -- yes --> R3["A warm coat"]
  C -- no --> R4["Stay inside!"]
  R1 --> E([print advice])
  R2 --> E
  R3 --> E
  R4 --> E
    

The conditional expression

When you just need to choose between two values, the conditional expression (sometimes called the ternary operator) does it in one line: value_if_true if condition else value_if_false.

Repeating with while

A loop repeats a block of code. Each repetition is called an iteration. A while loop repeats as long as its condition stays truthy; it checks the condition before every iteration.

Step through and watch the condition being checked four times: three times it is true, the fourth time (count is 0) it is false and the loop ends.

while is the right choice when you do not know in advance how many iterations you need: "keep asking until the input is valid", "keep halving until the number is small".

Iterating with for and range

A for loop takes each item of an iterable (anything you can loop over: a string, a list, a range, a file…) in turn, binds it to a loop variable, and runs the block. You do not manage a counter, so there is nothing to forget.

range

range produces a sequence of integers without building a list in memory:

Just like slices, the stop value is excluded. That makes range(len(s)) exactly the valid indexes of s.

enumerate: index and item together

When you need the position as well as the item, use enumerate instead of indexing by hand:

for i in range(len(fruits)):
    print(i, fruits[i])
for i, fruit in enumerate(fruits):
    print(i, fruit)

The accumulator pattern

Many loops build up a result: start with an empty value, then update it each iteration. This is called the accumulator pattern.

break, continue, and loop else

Nested loops

A loop inside a loop runs the inner loop completely for each iteration of the outer loop. With 3 outer and 4 inner iterations, the inner body runs 12 times. break only leaves the innermost loop.

The walrus operator :=

The assignment expression name := value, nicknamed the walrus operator because := looks like a walrus's eyes and tusks, assigns and produces the value, so you can use it inside a condition. Its most common use is a loop that reads until a stop signal:

line = input("> ")
while line != "quit":
    print("You said", line)
    line = input("> ")     # duplicated
while (line := input("> ")) != "quit":
    print("You said", line)

Structural pattern matching: match/case

A match statement compares a value (the subject) against a series of patterns, top to bottom, and runs the block of the first pattern that fits. It is far more than a "switch" statement: patterns can check the shape of data and pull pieces out of it into names. This is called destructuring.

Literal, OR, and wildcard patterns

Capture and sequence patterns

A bare name in a pattern is a capture pattern: it matches anything and binds it to that name. A list-like pattern [a, b] matches a sequence of exactly that length, and *rest captures any remaining items.

Guards

A guard is an if after a pattern; the case only matches if the guard is also true.

match point:
    case (x, y) if x == y:
        print("on the diagonal")
    case (x, y):
        print("somewhere else")

Mapping patterns

A dictionary-like pattern {"key": pattern} matches a dict that has those keys (extra keys are allowed). Dictionaries are covered fully in M06; for now, read {"type": "click", "x": 3} as "a record with fields". This is exactly how you handle JSON data from the web.

Class patterns

A class pattern such as int() or str(s) checks the type of the subject. Point(x=0, y=y) checks the type and its attributes. (You will write your own classes in M10; here the @dataclass line creates a simple Point type for us.)

Algorithmic thinking with flowcharts

An algorithm is a step-by-step procedure that solves a problem. Before writing a loop-heavy program, professionals sketch the algorithm: inputs, decisions, repetitions, and when to stop. A flowchart is one good way; plain-language steps (pseudocode) are another.

Here is the algorithm for this module's guessing game:

flowchart TD
  S([start]) --> P["pick secret number 1..100
attempts = 0"] P --> G["read a guess"] G --> V{"is it a valid number?"} V -- no --> M["print 'please enter a number'"] --> G V -- yes --> I["attempts += 1"] I --> C{"guess vs secret"} C -- "lower" --> L["print 'too low'"] --> G C -- "higher" --> H["print 'too high'"] --> G C -- "equal" --> W["print 'correct in N attempts'"] --> E([end])

Every arrow back to "read a guess" is a loop; every diamond is an if. Reading off the chart: we need a while loop (we do not know how many guesses), a validity check, and an if/elif/else comparison. Translating it:

Practice

Some exercises below ask you to complete a small function: a named block that receives inputs (parameters) and sends back a result with return. Functions let the tests try many inputs. You will learn them properly in M05; for now, write your code in the indented body and return the answer.

Print the numbers 1 to 15, one per line, but print Fizz instead of multiples of 3, Buzz instead of multiples of 5, and FizzBuzz for multiples of both.

Hint

Check the "both" case first (n % 15 == 0), otherwise a multiple of 15 would be caught by the "Fizz" branch.

Show solution
for n in range(1, 16):
    if n % 15 == 0:
        print("FizzBuzz")
    elif n % 3 == 0:
        print("Fizz")
    elif n % 5 == 0:
        print("Buzz")
    else:
        print(n)

Complete letter_grade(score) so it returns "A" for 90 and above, "B" for 80–89, "C" for 70–79, "D" for 60–69, and "F" below 60. Scores outside 0–100 return "invalid".

Hint

Check for invalid scores first: if not 0 <= score <= 100: return "invalid". Then the elif chain from high to low.

Show solution
def letter_grade(score):
    if not 0 <= score <= 100:
        return "invalid"
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    elif score >= 60:
        return "D"
    return "F"

print(letter_grade(85))

Complete digit_sum(n) to return the sum of the decimal digits of a non-negative integer, using a while loop with % and // (no strings). digit_sum(9875) is 29; digit_sum(0) is 0.

Hint

n % 10 is the last digit and n // 10 removes it. Loop while n > 0.

Show solution
def digit_sum(n):
    total = 0
    while n > 0:
        total += n % 10
        n //= 10
    return total

Complete first_long_word(text, length): return the first word in text with at least length characters, or None if there is none. Use a for loop with break or return. Bonus: try writing it with a loop else.

Hint

if len(word) >= length: return word

Show solution
def first_long_word(text, length):
    for word in text.split():
        if len(word) >= length:
            result = word
            break
    else:
        result = None
    return result

Complete respond(command) using a match on command.split():

  • "go north" → "Going north" (any single direction word)
  • "take lamp key" → "Taking lamp, key" (one or more items, joined with ", ")
  • "quit" or "exit" → "Bye"
  • "" (no words) → "Say something"
  • "add 2 3" → "5": two whole numbers added (use a guard with isdigit())
  • anything else → "Unknown command"
Hint

Use case ["take", first, *rest]: to require at least one item, an OR pattern case ["quit" | "exit"]:, and case ["add", a, b] if a.isdigit() and b.isdigit():.

Show solution
def respond(command):
    match command.split():
        case []:
            return "Say something"
        case ["go", direction]:
            return f"Going {direction}"
        case ["take", first, *rest]:
            return "Taking " + ", ".join([first, *rest])
        case ["quit" | "exit"]:
            return "Bye"
        case ["add", a, b] if a.isdigit() and b.isdigit():
            return str(int(a) + int(b))
        case _:
            return "Unknown command"

Mini-project: a guessing game and a menu calculator

Mini-projectGuessing game + text-menu calculator

Two small interactive programs in your practice workspace. Run them with pixi run python guess.py and pixi run python calc.py, and add a task for each.

Part A: guess.py

  • Pick a secret with import random and secret = random.randint(1, 100).
  • Loop until the player guesses it, replying "Too low" or "Too high"; ignore non-numeric input with a friendly message (str.isdigit()) without counting it as an attempt.
  • Limit the player to 7 attempts; use a loop else (or a flag) to reveal the number if they run out.
  • At the end, ask "Play again? (y/n)" and start over on y.
I'm thinking of a number between 1 and 100. You have 7 attempts.
Guess 1: 50
Too high.
Guess 2: 25
Too low.
Guess 3: forty
Please enter a whole number.
Guess 3: 37
Correct! You got it in 3 attempts.
Play again? (y/n) n
Thanks for playing!

Part B: calc.py

  • Show a menu (1) add 2) subtract 3) multiply 4) divide 5) power q) quit) in a loop.
  • Use match on the choice, with an OR pattern for "q" | "quit".
  • Read two numbers with float(input(...)) and print the result with an f-string.
  • Refuse division by zero with a message instead of crashing.
  • Use the walrus operator for the menu loop: while (choice := input("Choice: ").strip()) not in ("q", "quit"):

Acceptance criteria

  • You drew (on paper or in Mermaid) a flowchart for the guessing game before coding it
  • The game gives correct too-low/too-high hints and counts only valid attempts
  • Running out of attempts reveals the secret; "Play again" works
  • The calculator menu repeats until q or quit
  • The calculator dispatches with match/case, including an OR pattern and a case _ for invalid choices
  • Dividing by zero prints a message and the program keeps running
  • Both scripts run via Pixi tasks and are formatted with pixi run ruff format

Stretch goals

  • Let the player choose a difficulty that changes the range and the number of attempts.
  • Make the computer guess your number using binary search, and count how many guesses it needs.
  • Let the calculator accept a single line like 3 * 4 and parse it with match line.split():.

Quiz

Recap

Going further