Track B — Python Internals and Runtime Behavior
Reported: Python internals surfaced during the systems-flavored coding round, specifically generators, async constructs, and iterators (
../../research/source-report.mdrows 24–27).Rule for this entire track: no claim without a script that demonstrates it. Every statement below is backed by a file in
experiments/that you run and watch. Prose about the runtime is how you end up confidently wrong in an interview.
→ Study guide: WARMUP.md — the CPython runtime from the interpreter up. → QUIZBANK.md — 150 questions with full mechanism-level answers.
Table of Contents
- How These Questions Actually Arrive
- Concept Inventory
- The Experiments
- Drill Set
- Quiz Bank
- Failure Modes
- Self-Assessment Rubric
- References
How These Questions Actually Arrive
This is the framing that makes the track efficient, and it is inference I4 in
../../research/findings.md.
The reported observations — "Coding 2 was systems-flavored: state management, concurrency, memory efficiency" and "Python internals came up: generators, async, iterators" — are not two separate facts. They are one fact: you are asked to build a stateful streaming component, and the internals questions arise from your own implementation choices.
Nobody asks "explain the descriptor protocol." They ask:
"You used a generator here instead of returning a list. What does that buy you, and what does it cost?"
"This is an async function but you called a blocking library. What happens?"
"You're holding all of these in memory. How much is that actually?"
So preparing internals as trivia is the wrong shape and will feel like wasted work when the round comes. Preparing them as justifications for choices you made thirty seconds ago is the right shape. Every drill in this track is phrased that way.
Concept Inventory
B1. Iterators and Generators
| Concept | Proven by | The interview form of the question |
|---|---|---|
Iterable vs iterator; why iter(x) is x for one | exp01_generators.py | "Why can I only loop over your object once?" |
The iterator protocol and StopIteration | exp01 | "What does for actually compile to?" |
| Generator functions as suspended frames | exp01 | "Where does the local state live between yields?" |
send() and priming | exp01 | "Why does sending to a fresh generator raise?" |
throw() and its three outcomes | exp01 | "What happens if the generator catches it?" |
close() and GeneratorExit | exp01 | "Does your finally run if the consumer stops early?" |
yield from delegation and the return value | exp01 | "Where does the sub-generator's return value go?" |
| Generators as state machines | exp01 | "Rewrite this class-based state machine as a generator" |
itertools.tee as a memory hazard | exp01 | "You teed the stream. What is that buffering?" |
Async generators and aclosing | exp02_async.py | "Who runs the cleanup in your async generator?" |
B2. Async and the Event Loop
| Concept | Proven by | The interview form |
|---|---|---|
| The loop as a ready-callback queue | exp02_async.py | "Walk me through what happens when you await" |
| Coroutine vs Task vs Future | exp02 | "Does calling a coroutine run it?" |
gather orphans siblings on failure | exp02 | "One of your five tasks raised. What happened to the other four?" |
TaskGroup and structured concurrency | exp02 | "Why is TaskGroup a bug fix and not a style choice?" |
ExceptionGroup and except* | exp02 | "How do you handle three simultaneous failures?" |
CancelledError inherits BaseException | exp02 | "Why doesn't except Exception catch cancellation?" |
| Cancellation is cooperative | exp02 | "How would you write an uncancellable task by accident?" |
| Fire-and-forget tasks get garbage collected | exp02 | "Why did that task never finish?" |
| A blocking call stalls the whole loop | exp02 | "You called requests.get in a coroutine" |
to_thread / executors as the escape hatch | exp02 | "So how do you call a blocking library?" |
B3. Concurrency and the GIL
| Concept | Proven by | The interview form |
|---|---|---|
| What the GIL guarantees, and what it does not | exp03_gil.py | "Is counter += 1 thread-safe?" |
list.append atomic; x += 1 not — at the bytecode level | exp03 | "Show me the bytecode" |
| Threads give no CPU parallelism under the GIL | exp03 | "You added threads and it got slower. Why?" |
| Processes: cost of IPC and startup | exp03 | "When is multiprocessing not worth it?" |
| Free-threaded builds: PEP 703, PEP 779, 3.14, Phase II | exp03 | "Is the GIL gone yet?" |
sys._is_gil_enabled() | exp03 | "How would you check at runtime?" |
| The threads/processes/async decision table | below | "Which would you reach for here?" |
asyncio sync primitives are not thread-safe | exp02 | "Can I share an asyncio.Lock across threads?" |
The decision table
| Model | Wins on | The specific cost that makes it lose |
|---|---|---|
| asyncio | Thousands of concurrent I/O waits; high-fan-out RPC | One blocking call stalls everything; needs an async call stack all the way down |
| Threads | Blocking I/O through non-async libraries; moderate concurrency | No CPU parallelism under the GIL; ~8MB stack each; shared-mutable-state bugs |
| Processes | CPU-bound work | Serialization on every call; memory duplication; slow startup; no shared objects |
One line to remember: async for waiting, processes for computing, threads for when the library gives you no choice.
B4. Memory and the Object Model
| Concept | Proven by | The interview form |
|---|---|---|
| Reference counting + the cycle collector | exp04_memory.py | "When is this freed?" |
__del__ in cycles; PEP 442 changed this in 3.4 | exp04 | "Does your destructor run in a cycle?" |
Why weakref.finalize beats __del__ | exp04 | "How do you run cleanup safely?" |
__slots__: what it removes, what it breaks | exp04 | "How much memory did that actually save?" |
A non-slotted subclass regains __dict__ | exp04 | "Your subclass undid the optimization" |
sys.getsizeof vs real footprint | exp04 | "How much is this list of strings costing?" |
tracemalloc for real attribution | exp04 | "Show me, don't tell me" |
memoryview and the buffer protocol | exp04 | "How do you slice 100MB without copying it?" |
| Small-int and string interning | exp04 | "Why is a is b True here and False there?" |
| pymalloc arenas: freed ≠ returned to the OS | exp04 | "I freed everything and RSS didn't drop" |
B5. The Data Model
| Concept | Proven by | The interview form |
|---|---|---|
| Attribute lookup order | exp05_datamodel.py | "Rank: instance dict, data descriptor, non-data descriptor" |
| Data vs non-data descriptors | exp05 | "Why can't I shadow a @property?" |
__getattr__ vs __getattribute__ | exp05 | "Which one is the performance hazard?" |
The __getattribute__ recursion bug | exp05 | "Why does this hang?" |
| MRO and C3 linearization | exp05 | "Which __init__ runs?" |
super() is not "the parent class" | exp05 | "What does super() actually resolve to?" |
Context managers and __exit__'s return value | exp05 | "How do you suppress an exception?" |
weakref and what cannot be weak-referenced | exp04 | "Build a cache that doesn't leak" |
B6. Performance
| Concept | Proven by | The interview form |
|---|---|---|
| Generators vs lists: measured memory | exp04 | "Why a generator here?" |
functools.lru_cache and its keying | exp05 | "What's the cache key? What if an arg is unhashable?" |
| Streaming file/network processing | Track A streaming-parser | "The file is 40GB" |
| When to reach for a C extension | this file | "This loop is the bottleneck. Now what?" |
dis as a debugging tool | exp03 | "Prove x += 1 isn't atomic" |
The Experiments
cd tracks/python-internals/experiments
python3 exp01_generators.py # iterator protocol, send/throw/close, yield from
python3 exp02_async.py # loop mechanics, gather vs TaskGroup, cancellation
python3 exp03_gil.py # GIL, bytecode, threads vs processes, free-threading
python3 exp04_memory.py # refcounts, cycles, __slots__, memoryview, tracemalloc
python3 exp05_datamodel.py # descriptors, MRO, __getattr__ vs __getattribute__
Each script prints a claim, then the evidence for it, then the interview-form question it answers. They are meant to be read and modified, not just run — the drill is to predict each output before it prints.
Drill Set
| Drill | Cadence | What it trains |
|---|---|---|
| Predict-then-run | Daily, 10 min | Open an experiment, predict every output, then run. Every miss goes to ../../review/ at 1 day |
| Justify the choice | With every Track A problem | After solving, answer out loud: why a generator here? what does this cost in memory? what breaks if this is called from two threads? |
| Break it deliberately | Weekly | Introduce the bug (blocking call in a coroutine, except Exception swallowing cancellation, a non-slotted subclass) and observe the symptom. Recognizing symptoms is what you need in a room |
| Bytecode read | Weekly | dis something you wrote and explain the interpreter's steps |
| Measure, don't assume | Weekly | Any memory or speed claim you make gets a tracemalloc or perf_counter script before you say it out loud |
| Quiz pass | Weekly | 20 questions from the bank, closed book, confidence-marked |
Quiz Bank
The 20-question diagnostic in ../../diagnostics/d3-python-internals.md
is the seed, with full answers in
../../diagnostics/ANSWER-KEY.md.
The bank grows to 150+ across the program, one section per inventory area above, generated in batches as each area is drilled. Every question carries: the answer, the mechanism (not just the outcome), and the runnable proof.
The scoring rule that matters: track confident-wrong separately from correct. A gap you know about is a study item. A gap you are confident about is what you will assert in an interview and be corrected on — and the correction costs far more than the admission would have. Confident-wrong answers enter the review queue at the 1-day interval, ahead of everything else.
Failure Modes
| Failure | Symptom | Fix |
|---|---|---|
| Trivia framing | You can define a descriptor but cannot say why your code needed one | Reframe every item as a justification for a choice |
| Confidently wrong | High confidence, wrong answer, especially on GIL and async | Predict-then-run, daily |
| Version staleness | Answering the free-threading question from a 2023 memory | Re-verify with sys._is_gil_enabled() and the PEPs; this changes yearly |
| Prose belief | "Generators save memory" with no number | Measure it. exp04 does |
| Async cargo cult | async on everything, including CPU-bound code | Read the decision table; run exp02's blocking-call demo |
| Swallowed cancellation | except Exception in a coroutine, tasks that will not die | exp02's cancellation section |
__slots__ theatre | Adding slots without measuring, and with a non-slotted subclass | exp04 measures both |
Self-Assessment Rubric
| Level | Standard |
|---|---|
| L0 | ≤7/20 on the quiz, or ≥4 confident-wrong |
| L1 | 8–12/20; can state outcomes but not mechanisms |
| L2 | 13–16/20; explains mechanisms; ≤1 confident-wrong |
| L3 | 17–20/20; explains mechanisms, knows the version-dependent answers, and reaches for a measurement rather than an assertion |
Hire-bar translation
| Verdict | What it looks like |
|---|---|
| No hire | "Generators are lazy" and nothing beneath it |
| Hire (senior) | Correct outcomes; some mechanism; honest about gaps |
| Strong hire (senior) | Mechanism-level for generators, async, and the GIL; correct on version-dependent facts |
| Hire (staff) | Above, plus connects each to a design decision made under real constraints |
| Strong hire (staff) | Above, plus reaches for dis or tracemalloc unprompted to settle a question rather than asserting |
References
- Ramalho, L. Fluent Python, 2nd ed. — Ch. 17 (iterators/generators), Ch. 19 (concurrency models), Ch. 21 (async), Ch. 23 (descriptors)
- Slatkin, B. Effective Python, 3rd ed. — Items on generators, concurrency, and memory
- PEP 380 — Delegating to a Subgenerator. https://peps.python.org/pep-0380/
- PEP 442 — Safe Object Finalization. https://peps.python.org/pep-0442/
- PEP 492 / 525 — Coroutines; Async Generators.
- PEP 703 — Making the GIL Optional. https://peps.python.org/pep-0703/
- PEP 779 — Criteria for supported status for free-threaded Python. https://peps.python.org/pep-0779/
- CPython — Descriptor HowTo Guide. https://docs.python.org/3/howto/descriptor.html
- CPython — Python support for free threading. https://docs.python.org/3/howto/free-threading-python.html
- Python Free-Threading Guide. https://py-free-threading.github.io/
- Shaw, N. / CPython devguide — Garbage Collector Design. https://devguide.python.org/internals/garbage-collector/
- Beazley, D. Generators: The Final Frontier. https://www.dabeaz.com/finalgenerator/