Python from First PrinciplesPost 4 of 20 · 20 of 20 written

4. Control flow and expressions

Python · 4 of 202 min readOct 2026

Python’s control flow is small; the leverage is in its idioms. Truthiness, comprehensions, unpacking and “ask forgiveness” error handling make up most idiomatic code.

Truthiness

if x: calls bool(x), which uses the object’s __bool__ method, or __len__ if there is none. The falsy values are None, False, 0, 0.0, "", [], {}, set() and empty ranges; everything else is truthy.

The trap: if not value: treats a valid 0 or "" as missing. A real bug: temperature = temperature or 0.7 silently turns a deliberate temperature of 0 into 0.7. When zero is a legitimate value, write if value is None:.

Boolean operators return operands

a or b returns a if it is truthy, otherwise b. a and b returns a if it is falsy, otherwise b. Both short-circuit: b is never evaluated if the answer is already known. Comparisons chain: 0 < lr < 1.

Loops

  • for iterates over any iterable (Part 6). There is no C-style index loop, and you rarely need one.
  • enumerate(xs) gives index and value; zip(a, b, strict=True) walks sequences in parallel and raises if their lengths differ. Use strict=True for predictions and labels, because a plain zip silently stops at the shorter one.
  • range(n) is lazy and uses constant memory.
  • for ... else: the else runs only if the loop finished without break. It is useful in search loops.
  • while suits conditions, polling and retry loops.

Comprehensions

squares = [x * x for x in range(10) if x % 2 == 0]
vocab = {tok: i for i, tok in enumerate(tokens)}
unique = {w.lower() for w in words}
total = sum(len(doc) for doc in docs)   # generator expression: no list is built

Comprehensions are faster than the equivalent append loop and read as one idea. Keep them to one or two clauses; beyond that, write a normal loop.

Unpacking

x, y = point
first, *rest = tokens
a, b = b, a                                   # swap without a temp variable
for i, (name, score) in enumerate(results.items()): ...
batch_size, seq_len, d_model = x.shape        # everywhere in ML code
merged = {**defaults, **overrides}            # later keys win

Conditional expressions and the walrus operator

  • label = "pos" if score > 0 else "neg" is an expression, so it fits inside other expressions.
  • := assigns inside an expression: while (chunk := f.read(8192)): reads until the file is empty.

Structural pattern matching (3.10+)

match destructures data by shape. It is handy for routing streamed LLM events and tool calls:

match event:
    case {"type": "text", "text": str(t)}:
        print(t, end="")
    case {"type": "tool_use", "name": name, "input": args}:
        run_tool(name, args)
    case _:
        pass

It is not a C switch. Each pattern checks structure and binds names when it matches.

EAFP versus LBYL

  • LBYL, “look before you leap”: if key in d: v = d[key].
  • EAFP, “easier to ask forgiveness than permission”: try: v = d[key] followed by except KeyError:.

Python culture favors EAFP. It avoids race conditions (a file can vanish between the check and the open), and it is fast when failures are rare. Use LBYL when failure is common or the check is cheap and clearer.

Questions to ask:

  • Can a valid value be falsy here?
  • Should these sequences have equal length, and is zip silently truncating?