Control flow

Write loops the way Python wants them written — over the things themselves, with the clauses most people never learn.

07of 24
≈ 12Min read
5Parts

Conditions

and and or do not return True and False — they return one of their operands. a or "default" is a common idiom, and it is also a bug when a is legitimately 0 or "".

Python has no switch; since 3.10 it has match, which is pattern matching rather than a switch and earns its place on structures, not on single values.

py
if score >= 90:
    grade = "A"
elif score >= 80:          # elif, not "else if"
    grade = "B"
else:
    grade = "C"

grade = "A" if score >= 90 else "B"     # the conditional expression

if 0 <= score <= 100:      # chained comparison — really means both
    ...

if name and name.strip():  # and/or short-circuit, and return an operand
    ...

Loop over the thing, not over an index

range is lazy: range(10**9) allocates nothing. zip stops at the shortest input, which silently loses data — strict=True turns that into an error, and you almost always want it.

py
for word in words:                     # not for i in range(len(words))
    print(word)

for i, word in enumerate(words):       # when you genuinely need the index
    print(i, word)

for i, word in enumerate(words, start=1):
    print(f"{i}. {word}")

for name, age in zip(names, ages):     # two sequences in step
    ...

for name, age in zip(names, ages, strict=True):   # 3.10+: raise if lengths differ
    ...

for i in range(5):        # 0 1 2 3 4
for i in range(2, 10, 3): # 2 5 8
for _ in range(3):        # "do this three times"

break, continue, and the else nobody knows

for ... else runs its else when the loop finished normally. It reads badly — think of it as "no break" — but it is the clean way to write a search: do the work in the loop, and handle "not found" in the else without a flag variable.

py
for item in items:
    if item.broken:
        break              # leave the loop
    if item.skip:
        continue           # next iteration
else:
    print("finished without breaking")   # runs only if break did NOT happen

while, and the walrus

:= (the walrus) exists so that a value you test is also a value you keep, without repeating the call or hoisting it out of the condition. Use it in a while head and in a comprehension filter; do not scatter it.

py
while queue:               # an empty container is false: no len() needed
    job = queue.pop()
    handle(job)

while True:
    line = read()
    if not line:
        break
    handle(line)

while (line := read()):    # := assigns and evaluates, in one place
    handle(line)

match, for structure

match destructures as it matches, binds the names on the way and supports a guard. For one value against five constants, a dict lookup is still shorter and clearer.

py
match event:
    case {"type": "click", "x": x, "y": y}:
        handle_click(x, y)
    case {"type": "key", "key": k} if k.isalpha():
        type_letter(k)
    case [first, *rest]:
        handle_list(first, rest)
    case _:
        ignore()

Practice