Contents
3 Chapter

Control flow — if, while, for, match-case

Flow control where blocks come from indentation, range/enumerate/zip, and match-case pattern matching that differs in feel from switch.

Building on the variables from Chapter 2 Variables, basic types, and type hints, this chapter covers flow: if, while, for, and the match-case added in 3.10.

The match-case you’ll meet often later is the key point of this chapter. It feels different from switch in other languages and is unfamiliar at first, but once you’re used to it, JSON / event / DTO branching code gets short and safe. Chapter 13 Pattern matching in depth revisits this in earnest.

Two big features of Python’s control flow:

  1. Blocks are delimited by indentation. No {} braces.
  2. Headers end with a colon (:). Like if x > 0:.

Indentation is the syntax. Conventionally 4 spaces (PEP 8). Mixing tabs and spaces causes errors.

if / elif / else #

The most similar to other languages. The difference is no parentheses.

if basics
score = 85

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

print(grade)   # B

Not else if but elif. Why it’s contracted is historical — just memorize it.

One-liner — the ternary operator #

Different from C-family cond ? a : b. Python is a if cond else b.

ternary
status = "adult" if age >= 19 else "minor"

The natural-language ordering feels odd at first, but reads well once you’re used to it.

Truthiness and conditions — using falsy #

Reuse the falsy values from Chapter 2.

falsy conditions
items: list[int] = []

if not items:
    print("empty")    # this prints

# Same meaning as below, but the above is more Pythonic
if len(items) == 0:
    print("empty")

Using len(x) == 0 to check emptiness is usually shortened to not x.

is and == #

This part needs care.

is vs ==
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)   # True   (equal values)
print(a is b)   # False  (different objects)

x = None
print(x is None)   # True  ← None comparison is always is

== is value comparison, is is object identity comparison. Always compare None, True, False with is. Use == for other values.

while #

Repeats while the condition is true.

while basics
count = 0
while count < 5:
    print(count)
    count += 1

There’s no ++. It’s count += 1.

break, continue #

break / continue
n = 0
while True:
    n += 1
    if n % 2 == 0:
        continue       # skip evens
    if n > 7:
        break          # stop past 7
    print(n)
# 1, 3, 5, 7

The while True + break pattern is for “infinite loops whose condition is too complex to write up top”. Common in practice.

The else clause — a less-known feature #

while and for can have an else. It runs “when the loop ended without a break”.

while-else
n = 0
while n < 10:
    if n == 99:
        break
    n += 1
else:
    print("ended without break")    # this prints

Not common, but sometimes clean for “the searched value wasn’t found after a full scan”. Used too much, readability suffers, so use sparingly.

for — always iterates a sequence #

There is no C-style for (i = 0; i < n; i++) in Python. Python’s for always iterates an iterable.

for basics
names = ["a", "b", "c"]
for name in names:
    print(name)

Strings are iterable too.

character by character
for ch in "hello":
    print(ch)
# h, e, l, l, o

Chapter 11 Iterables, generators, yield from covers in depth what objects for in works on (the __iter__ / __next__ protocol).

range — when you need a numeric sequence #

range
for i in range(5):       # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 7):    # 2, 3, 4, 5, 6
    print(i)

for i in range(0, 10, 2):  # 0, 2, 4, 6, 8 (step 2)
    print(i)

It’s range(start, stop, step). stop is not included (half-open interval). This is confusing at first, but it’s the same convention as slices in other languages, so you’ll get used to it.

enumerate — when you need an index #

when you need an index
items = ["apple", "pear", "persimmon"]
for i, item in enumerate(items):
    print(f"{i}: {item}")
# 0: apple
# 1: pear
# 2: persimmon

Writing for i in range(len(items)) then items[i] is an antipattern. enumerate does it in one word.

zip — iterating multiple lists together #

zip
names = ["curtis", "smith", "john"]
ages = [30, 25, 40]

for name, age in zip(names, ages):
    print(f"{name}: {age}")

If the lengths differ, it cuts off at the shorter side. If you want an error on mismatched lengths, use zip(..., strict=True) (3.10+).

strict mode
for n, a in zip(names, ages, strict=True):
    ...

Pattern matching — match-case (3.10+) #

The shape resembles switch, but it can do much more. Python is designed as pattern matching, not just multi-branch.

The simplest form #

simple branching
def http_status(code: int) -> str:
    match code:
        case 200:
            return "OK"
        case 404:
            return "Not Found"
        case 500:
            return "Server Error"
        case _:
            return "Unknown"

case _: is the wildcard (default). The equivalent of JavaScript’s default:.

Multiple values per case — | #

OR pattern
def category(code: int) -> str:
    match code:
        case 200 | 201 | 204:
            return "success"
        case 400 | 401 | 403 | 404:
            return "client error"
        case 500 | 502 | 503:
            return "server error"
        case _:
            return "other"

Destructuring — the real value starts here #

You can use the shape itself of lists, tuples, and dicts as a pattern.

destructuring
def describe(point: tuple[int, ...]) -> str:
    match point:
        case ():
            return "empty point"
        case (x,):
            return f"1D: {x}"
        case (x, y):
            return f"2D: ({x}, {y})"
        case (x, y, z):
            return f"3D: ({x}, {y}, {z})"
        case _:
            return "higher dimensional"

print(describe((1, 2)))      # 2D: (1, 2)
print(describe((1, 2, 3)))   # 3D: (1, 2, 3)

The x, y inside each pattern bind the value at that position to a variable. Matching and variable creation happen at once. JavaScript destructuring + switch combined into one.

Dict patterns #

dict pattern
event = {"type": "click", "x": 100, "y": 200}

match event:
    case {"type": "click", "x": x, "y": y}:
        print(f"click: ({x}, {y})")
    case {"type": "key", "code": code}:
        print(f"key: {code}")
    case _:
        print("unknown event")

A pattern where a specific key exists and its value is bound to a variable. Fits API response handling well.

Guards — adding an if condition #

guard
match (x, y):
    case (a, b) if a == b:
        print(f"equal: {a}")
    case (a, b) if a > b:
        print(f"a is greater")
    case _:
        print("other")

Class patterns #

class match
class Circle:
    def __init__(self, radius: float):
        self.radius = radius

class Square:
    def __init__(self, side: float):
        self.side = side

shape = Circle(5)

match shape:
    case Circle(radius=r):
        print(f"circle, radius {r}")
    case Square(side=s):
        print(f"square, side {s}")

Type check + attribute extraction in one line.

Why it differs in feel from switch #

match-case is not just multi-branch but a tool that branches on the shape of data. Rewriting the above with if-elif becomes long and awkward.

same thing without match
if isinstance(shape, Circle):
    r = shape.radius
    print(f"circle, radius {r}")
elif isinstance(shape, Square):
    s = shape.side
    print(f"square, side {s}")

Same behavior, but type checking and attribute extraction are separate. match combines these two steps into a single line. Especially valuable for JSON / event object handling.

This chapter only sees the surface. Chapter 13 Pattern matching in depth revisits the precise rules for sequence / mapping / class patterns and real-world use cases.

The truth about flow — pass #

A block cannot be empty. Without an indented block, you get a syntax error. To leave it empty for now, use pass.

leave empty
def todo():
    pass    # implement later

if x > 0:
    pass    # ignore for now
else:
    handle(x)

... (the Ellipsis object) is sometimes used for the same purpose, but pass is the more conventional choice.

for also has a pattern called comprehensions #

There’s a syntax that compresses for into one line when building lists, dicts, and sets.

quick preview
squares = [x ** 2 for x in range(5)]
# [0, 1, 4, 9, 16]

The next chapter, Chapter 4 Collections and comprehensions, covers it in earnest.

Exercises #

  1. In a project with pyright installed, write a function with the signature def grade(score: int) -> str:. It returns “A” for 90+, “B” for 80+, “C” for 70+, and “F” otherwise. Write one version with if-elif-else and one with match-case, and confirm they behave identically.
  2. A variable event of type dict[str, int | str] contains one of {"type": "click", "x": 100, "y": 200}, {"type": "key", "code": "Enter"}, or {"type": "scroll", "dx": 0, "dy": -10}. Write a handle(event) function that branches with match-case and dict patterns, printing a different message for each.
  3. With names = ["curtis", "smith", "john"] and ages = [30, 25, 40], combine enumerate + zip to print 0: curtis(30), 1: smith(25), 2: john(40). Apply strict=True so that mismatched lengths raise an error.

In one line: Python flow control is indentation + colon. if/elif/else, while, for in are the basics, and for’s companions range/enumerate/zip cover 90% of daily work. match-case is not a simple switch but a tool for branching on the shape of data, and it shines in JSON / event / class branching.

Next chapter #

In the next chapter, Chapter 4 Collections and comprehensions, we cover the four most-used data structures — list, tuple, dict, set — and the comprehensions that build them in one line. The for of this chapter is the basis of comprehensions.

X