The Zen of Python in Code Review

19 lessons from coaching developers and reviewing pull requests. What each line of import this looks like in real code.

The same mistakes surface constantly, not because developers are careless, but because Python's flexibility makes it easy to get things almost right. Here's a taste of what's inside.

Subscribe to get the full guide + monthly insights on Python, Rust, and AI.


Flat is better than nested

When your code makes an arrow shape, readability drops. Guard clauses flatten it so the happy path lives at the lowest indentation level.

# Nested (arrow anti-pattern)
def process(data):
    if data:
        if data.is_valid():
            if data.has_items():
                return handle(data)

# Flat
def process(data):
    if not data:
        return
    if not data.is_valid():
        return
    if not data.has_items():
        return
    return handle(data)

Errors should never pass silently

A silent failure is worse than a crash. A crash tells you where things went wrong.

# Silent failure: swallows network errors, JSON errors, server 500s
try:
    data = response.json()
except Exception:
    data = {}

# Fail fast: the stack trace is a gift — don't throw it away
response.raise_for_status()
data = response.json()

The standard library silences errors in unexpected places too. zip() silently truncates when iterables have different lengths:

letters = ("a", "b")
numbers = (1, 2, 3)

list(zip(letters, numbers))               # [('a', 1), ('b', 2)] — silent data loss
list(zip(letters, numbers, strict=True))  # ValueError: zip() argument 2 is longer

Readability counts

Raw tuples are positional landmines. result[0] tells you nothing. Swap two fields and the code still "works", just with wrong data.

# Unreadable: positional, no type safety
race_info = [(race.get('date'), race.get('raceName')) for race in races]
# caller: is race_info[0][0] the date or the name?

# Readable: attribute access, impossible to mix up
class Event(NamedTuple):
    name: str
    date: date

race_info = [
    Event(name=race.get("raceName", ""), date=_parse_date(race.get("date")))
    for race in races
]
# caller: event.name, event.date — self-documenting

Know your standard library

Most people reach for list.pop(0) as a queue because they don't know about deque. The difference isn't style, it's performance.

# O(n) per pop — every item shifts left
queue = list(range(100_000))
while queue:
    item = queue.pop(0)

# O(1) per popleft — built for this
from collections import deque

queue = deque(range(100_000))
while queue:
    item = queue.popleft()

Draining 100,000 items: 21.6ms with list.pop(0), 0.034ms with deque.popleft(). A 600x difference. The standard library is full of these: Counter, NamedTuple, Decimal, itertools. The Pythonic way is obvious once you've seen it.


The full guide covers all 19 lines of import this, each grounded in a real PR I reviewed, with before/after code. Subscribe to get it.

Subscribe to get the full guide + monthly insights on Python, Rust, and AI.


Bob Belderbos at PyCon in front of the Zen of Python

For more Python tips, browse my collection on GitHub. If you use Claude Code, you can also pull my tips directly into it via MCP.