After analyzing hundreds of Python interview questions and production bug reports, five patterns account for the majority of prediction errors and bugs that experienced developers encounter.
If you can trace each of these on sight, without hesitation, you are prepared for virtually any Python dry-run question.
Pattern 1: The Shared Mutable Default
def build_list(item, container=[]):
container.append(item)
return container
r1 = build_list("a")
r2 = build_list("b")
r3 = build_list("c", [])
print(r1)
print(r2)
print(r3)
Output:
['a', 'b']
['a', 'b']
['c']
r1 and r2 share the default list because it is created once at function definition time. r3 receives a new empty list explicitly so it is independent.
Trace rule: any call that does not pass the second argument shares the single default object.
Pattern 2: The Generator That Forgets
def make_gen(n):
for i in range(n):
yield i * 2
g = make_gen(4)
first_half = list(g)[:2]
second_half = list(g)
print(first_half)
print(second_half)
Output:
[0, 2]
[]
list(g) exhausts the generator. The first list(g) produces [0, 2, 4, 6] and then takes the first two elements. The generator is now exhausted. The second list(g) finds no remaining values.
Trace rule: calling list() on a generator always consumes it entirely.
Pattern 3: The Class Variable Mutation
class Registry:
items = []
@classmethod
def register(cls, item):
cls.items.append(item)
Registry.register("first")
r = Registry()
r.register("second")
Registry.register("third")
print(Registry.items)
print(r.items)
Output:
['first', 'second', 'third']
['first', 'second', 'third']
All three registrations go to the same class-level list. The instance r has no instance attribute items so it reads the class attribute. Both Registry.items and r.items reference the same list.
Trace rule: class variables are shared unless overridden by instance assignment.
Pattern 4: The Late Binding in a Loop
multipliers = {}
for i in range(1, 4):
multipliers[i] = lambda x: x * i
print(multipliers[1](10))
print(multipliers[2](10))
print(multipliers[3](10))
Output:
30
30
30
All three lambdas close over the same variable i. By the time they are called, the loop has finished and i is 3. Every lambda returns x * 3 regardless of which key you use.
Fix:
multipliers[i] = lambda x, i=i: x * i
The default parameter captures the current value of i at lambda creation time.
Pattern 5: The Chained Comparison With Side Effects
def check(n):
print(f"checking {n}")
return n
result = 1 < check(2) < 3
print(result)
Output:
checking 2
True
Python evaluates check(2) once and reuses the result in both comparisons. It does not call check twice. The expression becomes 1 < 2 and 2 < 3 which is True and True which is True.
This matters in production code when the middle expression has side effects: it runs exactly once.
The Unified Mental Model
All five patterns follow from the same underlying principle: Python names are references to objects, and the behavior you observe depends on whether operations create new objects or modify existing ones.
Default arguments, class variables, and closure variables are all cases where a single object is referenced from multiple places. Late binding is a case where the reference is resolved at call time, not at definition time. Chained comparison is a case where Python optimizes to avoid redundant evaluation.
Once you internalize references versus objects as your mental model, each pattern becomes predictable rather than surprising.
For daily practice on all five patterns and hundreds of variations, visit pycodeit.com. Free, no account needed to start.
Top comments (0)