Control Flow: Traps, Failure Modes & Interview Q&A

Master iteration failure modes, recursion stack limits, generator memory lifecycle pitfalls, decision criteria, and senior interview questions.

02. Control Flow, Pattern Matching, Iteration Protocol & Coroutines8 min read

Navigating control flow, iterators, and coroutines at scale requires a firm grasp of their operational limits, resource lifecycles, and subtle failure modes.

This lesson explores practical edge cases, explains why CPython omits tail-call optimization, provides a data pipeline decision matrix, and reviews key technical interview flashcards.

Default Recursion LimitRuntime Guard
1,000

Configured via sys.getrecursionlimit() to guard against native C stack overflow crashes.

1. Edge Cases & Architectural Failure Modes#

Trap 1: Recursion Limit & The Lack of Tail-Call Optimization#

Unlike functional languages (such as Scheme or Haskell), Python does not support Tail-Call Optimization (TCO). Every recursive call pushes a new PyFrameObject and consumes native C stack space:

1
2
3
4
5
6
7
8
9
10
11
12
13
import sys print(f"Current recursion limit: {sys.getrecursionlimit()}") def recursive_factorial(n: int) -> int: if n <= 1: return 1 return n * recursive_factorial(n - 1) try: recursive_factorial(2000) except RecursionError as e: print(f"Caught expected error: {e}")
Why Guido van Rossum Rejected TCO
CPython intentionally omits tail-call optimization to preserve full, unbroken stack tracebacks for debugging. In addition, Python's dynamic runtime (where any global function can be monkey-patched at runtime) makes static tail-call elimination complex and fragile.

Trap 2: Generator Exhaustion#

Iterators and generators are strictly single-pass data streams. Once exhausted, they cannot be rewound or restarted:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def number_stream(): yield 1 yield 2 yield 3 stream = number_stream() # First pass: Consumes the stream completely first_pass = list(stream) print(f"First pass : {first_pass}") # [1, 2, 3] # Second pass: Generator is already exhausted! second_pass = list(stream) print(f"Second pass: {second_pass}") # [] (Empty list!)

2. Data Pipeline Design Decision Matrix#

Workload / Use CaseArchitecture SelectionMemory ProfilePerformance Characteristics
Huge files / unbounded data streamsGenerator functions (yield)O(1)O(1) constant memoryLazy pull streaming; minimal RAM footprint.
Small in-memory transformationsList / Set / Dict ComprehensionsO(N)O(N) memoryFast C loop execution; optimized LIST_APPEND opcodes.
Declarative multi-shape parsingStructural Pattern Matching (match/case)O(1)O(1) auxiliaryFast jump-table matching with zero regex parsing overhead.
Stateful event consumer / processorCoroutine Generators (send)O(1)O(1) constant memoryCooperative push pipeline; decoupled from driver loops.
Intensive numerical array processingNumPy / Polars arraysCompact C contiguous buffersSIMD vectorization, cache locality, and zero PyObject overhead.

3. Senior Technical Interview Flashcards#

4. Concept Verification#

Test your understanding of iteration trade-offs and control flow limits:

Concept check

Why does CPython deliberately omit tail-call optimization (TCO) in recursive functions?

Concept check

What happens when you iterate over an already-exhausted generator a second time?