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.
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.
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.