Core Mechanics: Failure Modes, Trade-offs & Interview Q&A
Master CPython edge cases, memory trade-offs, mutable default arguments, closure late binding traps, and rigorous technical interview questions.
Writing robust, high-performance Python systems requires understanding the boundaries, failure modes, and architectural trade-offs inherent to CPython runtime mechanics.
This lesson explores classic implementation traps, provides a concrete decision matrix for data structures, and walks through high-frequency senior technical interview questions.
Suppressing instance __dict__ using __slots__ reduces instance memory footprint by up to 85%.
1. Classic CPython Failure Modes & Traps#
Failure Mode 1: Mutable Default Arguments#
Default parameter values in Python functions are evaluated once at definition time, not on every function call. They are stored inside the function code object's __defaults__ tuple:
1234567891011121314# Anti-pattern: Persistent state bug def append_item(val, bucket=[]): bucket.append(val) return bucket print(append_item(1)) # [1] print(append_item(2)) # [1, 2] - The list persists across function calls! # Idiomatic Solution: Sentinel pattern def append_item_clean(val, bucket=None): if bucket is None: bucket = [] bucket.append(val) return bucket
Failure Mode 2: Late Binding in Closures#
Variables captured by closures are looked up at the time the closure is called, not when the closure is defined:
12345678910# Bug: All lambdas evaluate to 4! multipliers = [lambda x: x * i for i in range(5)] print([m(2) for m in multipliers]) # Output: [8, 8, 8, 8, 8] # Explanation: 'i' is captured by reference in a PyCellObject. # When m(2) executes, the loop has completed and i is 4. # Solution: Bind immediately via default argument fixed_multipliers = [lambda x, i=i: x * i for i in range(5)] print([m(2) for m in fixed_multipliers]) # Output: [0, 2, 4, 6, 8]
2. "When to Use What" Data Structure Decision Matrix#
| Requirement / Constraint | Optimal Selection | Time Complexity | Memory Profile | Architectural Rationale |
|---|---|---|---|---|
| Dynamic array append operations | list (PyListObject) | amortized append | Over-allocates capacity ( average) | Contiguous pointers; rapid index access, but expensive internal insertions. |
| Fixed immutable sequence | tuple (PyTupleObject) | index lookup | Inline allocation, zero wasted capacity | Minimal memory footprint; hashable as dictionary key; free list reuse. |
| High-volume unique lookup | set (PySetObject) | membership test | Hash table overhead ( table size) | Open-addressing hash table providing instant membership verification. |
| Fast append / pop on both ends | collections.deque | push/pop left & right | Doubly linked 64-element block chunks | Avoids expensive memory shifts incurred when prepending to a list. |
| Lightweight record structures | NamedTuple or dataclass(slots=True) | attribute lookup | Suppresses per-instance __dict__ | Replaces dictionary hash lookup with direct struct offset access. |
3. Senior Technical Interview Flashcards#
4. Concept Verification#
Test your understanding of core mechanics and edge cases:
Why do functions with mutable default arguments (e.g. def fn(acc=[])) accumulate state across independent invocations?
What causes the late binding trap in closures when defining functions inside loops?