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:
- Blocks are delimited by indentation. No
{}braces. - Headers end with a colon (
:). Likeif 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.
score = 85
if score >= 90:
grade = "A"
elif score >= 80:
grade = "B"
elif score >= 70:
grade = "C"
else:
grade = "F"
print(grade) # BNot 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.
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.
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.
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.
count = 0
while count < 5:
print(count)
count += 1There’s no ++. It’s count += 1.
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, 7The 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”.
n = 0
while n < 10:
if n == 99:
break
n += 1
else:
print("ended without break") # this printsNot 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.
names = ["a", "b", "c"]
for name in names:
print(name)Strings are iterable too.
for ch in "hello":
print(ch)
# h, e, l, l, oChapter 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
#
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
#
items = ["apple", "pear", "persimmon"]
for i, item in enumerate(items):
print(f"{i}: {item}")
# 0: apple
# 1: pear
# 2: persimmonWriting for i in range(len(items)) then items[i] is an antipattern. enumerate does it in one word.
zip — iterating multiple lists together
#
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+).
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 #
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 — |
#
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.
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 #
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
#
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 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.
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.
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.
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 #
- In a project with
pyrightinstalled, write a function with the signaturedef grade(score: int) -> str:. It returns “A” for 90+, “B” for 80+, “C” for 70+, and “F” otherwise. Write one version withif-elif-elseand one withmatch-case, and confirm they behave identically. - A variable
eventof typedict[str, int | str]contains one of{"type": "click", "x": 100, "y": 200},{"type": "key", "code": "Enter"}, or{"type": "scroll", "dx": 0, "dy": -10}. Write ahandle(event)function that branches withmatch-caseand dict patterns, printing a different message for each. - With
names = ["curtis", "smith", "john"]andages = [30, 25, 40], combineenumerate+zipto print0: curtis(30),1: smith(25),2: john(40). Applystrict=Trueso that mismatched lengths raise an error.
In one line: Python flow control is indentation + colon.
if/elif/else,while,for inare the basics, andfor’s companionsrange/enumerate/zipcover 90% of daily work.match-caseis 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.