D3 — Python Internals Quiz

30 minutes. 20 questions. Closed book.

Predict before you run. Several questions ask for program output. Write your prediction first and run nothing until scoring. A question you got right by executing the code measures nothing — the whole point is to measure your model of the runtime, not the runtime.

Mark each answer certain / fairly sure / guessing. Confident-wrong is scored separately from admitted-gap, because they are different defects requiring different fixes.


Table of Contents


How to Answer

Two sentences per question is enough. What is being scored is whether you know the mechanism, not whether you can recite the manual. "It raises TypeError" is half a point; "it raises TypeError because a just-started generator is suspended before its first yield, so there is no expression waiting to receive the sent value" is the full point.

Assume CPython on a recent 3.x with the default (GIL-enabled) build unless a question says otherwise.


Section 1: Iterators and Generators

Q1. What happens, and why?

def echo():
    while True:
        received = yield
        print("got", received)

g = echo()
g.send("hello")

Q2. What does list(outer()) produce?

def inner():
    yield 1
    yield 2
    return "done"

def outer():
    result = yield from inner()
    yield result

Q3. Does cleanup print? Explain the exact mechanism, and name the exception involved.

def gen():
    try:
        yield 1
        yield 2
    finally:
        print("cleanup")

g = gen()
next(g)
del g

Q4. What does this print, and what is the design defect?

class Countdown:
    def __init__(self, n):
        self.n = n
    def __iter__(self):
        while self.n > 0:
            yield self.n
            self.n -= 1

c = Countdown(3)
print(list(c), list(c))

Q5. What is the value of next(a) on the last line, and why is that surprising?

a = iter([1, 2, 3, 4])
b = [10, 20]
pairs = list(zip(a, b))
next(a)

Q6. What is the difference between an iterable and an iterator? Why does iter(x) is x hold for one and not the other, and what breaks if you get it wrong?

Q7. You have a generator g that has already yielded three values. You call g.throw(ValueError("x")). Where does the exception appear from the generator's point of view, and what are the three possible outcomes?

Q8. Why is itertools.tee(it, 2) a memory hazard? Describe the situation in which it buffers the entire stream.


Section 2: Async and Concurrency

Q9. One of these two tasks raises. What happens to the other one?

async def boom():
    raise ValueError("boom")

async def slow():
    await asyncio.sleep(10)
    print("slow finished")

await asyncio.gather(boom(), slow())

Then: how does asyncio.TaskGroup differ, and what exception type does it raise?

Q10. asyncio.CancelledError — what does it inherit from, and what is the practical consequence for code that writes except Exception: inside a coroutine?

Q11. What is wrong with this, and what is the standard fix?

async def main():
    for url in urls:
        asyncio.create_task(fetch(url))
    await asyncio.sleep(5)

Name two independent defects.

Q12. A coroutine calls time.sleep(2) (not asyncio.sleep). Describe precisely what happens to the event loop and to every other pending task. Then name the correct escape hatch for genuinely blocking work.

Q13. What exactly does the GIL guarantee, and what does it not? Is counter += 1 on a shared integer thread-safe? Is some_list.append(x)? Explain the difference in terms of bytecode.

Q14. What is the current status of free-threaded CPython? Name the relevant PEPs, the version in which the status changed, whether it is the default build, and roughly what the single-threaded overhead is.

Q15. Give the decision rule for threads vs. processes vs. asyncio. For each, state the workload it wins on and the specific cost that makes it lose elsewhere.


Section 3: Memory and the Object Model

Q16. Does __del__ run for objects in a reference cycle? What changed, and in which Python version? What is the remaining hazard?

class Node:
    def __init__(self): self.ref = None
    def __del__(self): print("__del__ ran")

x = Node(); y = Node()
x.ref = y; y.ref = x
del x, y

Q17. What does __slots__ actually remove, what does it break, and what happens to the memory saving when a subclass of a slotted class does not itself declare __slots__?

Q18. Why is sys.getsizeof(some_list_of_10000_strings) misleading? What does it measure, and what would you use instead to answer "how much memory is this actually costing me?"


Section 4: Data Model and Performance

Q19. Attribute lookup precedence: rank these four in the order CPython consults them for instance.x — instance __dict__, data descriptor on the type, non-data descriptor on the type, __getattr__. Then explain why @property can shadow an instance attribute but a plain function cannot.

Q20. __getattr__ vs __getattribute__ — when is each called, which one is the performance hazard, and what is the classic infinite-recursion bug in __getattribute__?


Answer Sheet

Copy this, fill it in, and score against ANSWER-KEY.md.

QAnswer (2 sentences)ConfidenceCorrect?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Correct: ___ / 20 Confident-wrong (marked certain but wrong): ___

The second number is the one that matters most. A gap you know about is a study item. A gap you are confident about is a landmine — it is the thing you will assert in an interview and be corrected on, and the correction costs far more than the admission would have. Every confident-wrong answer goes straight into the review/ spaced-repetition queue at the 1-day interval.