Track B — Quiz Bank: 150 Questions With Full Answers

How to use this. Cover the answers. Say yours out loud. Mark each certain / fairly sure / guessing. Every miss goes into ../../review/ at the 1-day interval; every confident-wrong goes to the front of the queue, because that is the one you will assert in an interview and be corrected on.

Answers give the mechanism, not just the outcome. "It raises TypeError" is half a point.


Table of Contents


Section 1: Iterators and Generators (Q1–Q30)

Q1. What two methods does the iterator protocol require, and what exception ends it? __iter__ (returning self) and __next__. Iteration ends when __next__ raises StopIteration. That is the entire protocol — for is sugar over iter() then repeated next() in a try/except.

Q2. What does for x in thing: compile to? _it = iter(thing), then a loop calling next(_it) until StopIteration, running the body each time. iter() calls type(thing).__iter__.

Q3. Difference between an iterable and an iterator? An iterable has __iter__ returning a new iterator each call and holds no position. An iterator has both __iter__ (returning self) and __next__, and carries the cursor. So iter(x) is x for iterators only.

Q4. Why can some old classes be iterated without __iter__? The legacy sequence protocol: if there is no __iter__ but there is __getitem__, iter() builds an iterator calling x[0], x[1], … until IndexError.

Q5. What does calling a generator function do? Nothing except return a generator object. No body code runs until the first next().

Q6. What does a generator object hold? A suspended stack frame: locals, the instruction pointer, and the evaluation stack. next() resumes it; yield suspends it.

Q7. Why does g.send("x") on a fresh generator raise? TypeError: can't send non-None value to a just-started generator. A fresh generator is suspended before its first yield, so there is no yield expression waiting to receive the value. Prime with next(g) or g.send(None) first.

Q8. What are the three outcomes of g.throw(exc)? (a) The generator does not catch it → it propagates out of throw() and the generator closes. (b) It catches and yields again → throw() returns that value. (c) It catches and returns → throw() raises StopIteration.

Q9. What does g.close() do? Throws GeneratorExit at the suspended yield, so finally blocks run. If the generator catches GeneratorExit and yields again, Python raises RuntimeError: generator ignored GeneratorExit.

Q10. Does finally run if I break out of a for over a generator? Yes, in CPython — the loop drops its reference, refcount hits zero, the generator is finalized, which calls close(), which raises GeneratorExit at the yield. Promptness depends on refcounting, so on PyPy it happens whenever the GC runs. Use explicit close() or a context manager if you need portable determinism.

Q11. list(outer()) where inner yields 1, 2 and returns "done", and outer does r = yield from inner(); yield r? [1, 2, 'done']. yield from delegates iteration and the sub-generator's return value becomes the value of the yield from expression (PEP 380). 'done' is not yielded by inner; it is returned to outer, which yields it.

Q12. Name two things yield from does beyond a for loop. It forwards send/throw/close to the sub-generator, and it captures the sub-generator's return value. A for ... yield loop does neither.

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

class C:
    def __init__(self, n): self.n = n
    def __iter__(self):
        while self.n > 0:
            yield self.n; self.n -= 1
c = C(3); print(list(c), list(c))

[3, 2, 1] []. Each __iter__ call returns a fresh generator, but they all read and mutate the same self.n, which the first pass drove to 0. Iteration state must be local: for i in range(self.n, 0, -1): yield i.

Q14. a = iter([1,2,3,4]); b=[10,20]; list(zip(a,b)); next(a) — what is next(a)? 4. zip pulled 1 and 2 and paired them, then pulled 3 from a, asked b for a third item, got StopIteration, and stopped — discarding the 3. zip consumes one extra item from every iterator before the shortest one ends.

Q15. Why is itertools.tee a memory hazard? It buffers every item one branch has read that the other has not. Drain one branch fully and the internal deque holds the whole stream — you have materialized what the iterator existed to avoid. Safe only when branches advance in lockstep.

Q16. What is PEP 479 and what did it change? A StopIteration escaping from inside a generator body used to silently end the generator. Since 3.7 it becomes a RuntimeError. Practical rule: never call bare next() inside a generator unless you intend to end it.

Q17. Are generators thread-safe? No. Two threads calling next() can interleave and corrupt the frame; CPython raises ValueError: generator already executing when it detects it. Use a lock or one generator per thread.

Q18. How do you make a class both iterable and reusable? __iter__ returns a new iterator (typically by being a generator function) and stores no iteration state on the instance.

Q19. What does iter(callable, sentinel) do? The two-argument form calls callable() repeatedly until it returns sentinel. Useful for iter(lambda: f.read(4096), b'').

Q20. Memory of a list comprehension vs a generator expression over 2M items? Measured: ~77 MiB for the list, ~400 bytes for the generator — the generator holds one frame. The caveat is that the generator is only cheaper if you never need the data twice; re-iterating re-computes.

Q21. What is yield's value when nothing is sent? None. next(g) is equivalent to g.send(None).

Q22. How would you implement enumerate yourself?

def enumerate_(it, start=0):
    n = start
    for x in it:
        yield n, x
        n += 1

Q23. What is a generator-based state machine and why prefer it? The suspension point is the state, so there is no explicit state variable and no dispatch table — locals persist across yields. It is dramatically less code than the class-based equivalent for protocol parsing.

Q24. itertools.chain vs + for lists? chain is lazy and works on any iterables, allocating nothing. + materializes a new list.

Q25. What does islice not support that list slicing does? Negative indices — it cannot count from the end without consuming the whole iterator.

Q26. Why does sum(x*x for x in range(n)) avoid a list but sum([x*x for x in range(n)]) not? The first is a generator expression consumed lazily by sum; the second builds the whole list first. Same result, different peak memory.

Q27. What happens if a generator's finally blocks forever? close() blocks, and so does the garbage collector's finalization. At interpreter shutdown it can hang the process. Never block indefinitely in generator cleanup.

Q28. Can you restart an exhausted generator? No. Once it raises StopIteration it stays exhausted. Call the generator function again for a fresh one — which is why factories are passed around rather than generator objects.

Q29. What is gi_frame and when is it None? The generator's frame object. It is None once the generator is exhausted or closed — a way to test whether a generator is still live.

Q30. Why do generators make backpressure natural? The consumer controls the pace: nothing is produced until next() is called. Producer and consumer are coupled by demand rather than by a buffer, so there is no queue to grow unbounded.


Section 2: Async (Q31–Q60)

Q31. What is an event loop, in one sentence? A queue of ready callbacks plus one blocking call into the OS (epoll/kqueue) that wakes when a registered file descriptor is ready or a timer fires.

Q32. Is asyncio parallel? No. It is single-threaded concurrency by interleaving. Two coroutines never execute simultaneously.

Q33. Does calling an async def function run it? No. It returns a coroutine object, inert until awaited or scheduled. Never awaiting it produces a RuntimeWarning: coroutine was never awaited.

Q34. Coroutine vs Task vs Future? A coroutine is the inert object from async def. A Task wraps a coroutine so the loop steps it concurrently. A Future is a placeholder for a later result; Task subclasses Future.

Q35. Difference between await coro and asyncio.create_task(coro)? await runs it now, inline, sequentially. create_task schedules it to run concurrently and returns immediately.

Q36. Why is for url in urls: await fetch(url) a performance bug? It is fully sequential — each await completes before the next starts. Use await asyncio.gather(*(fetch(u) for u in urls)) or a TaskGroup.

Q37. Why can a fire-and-forget create_task vanish? The loop holds only a weak reference. Without a strong reference of your own the task can be garbage collected mid-execution. Keep a set, add a done-callback to discard, or use a TaskGroup.

Q38. What does asyncio.CancelledError inherit from? BaseException, since Python 3.8 — not Exception. So except Exception does not swallow it.

Q39. Why is that inheritance deliberate? Because swallowing cancellation would make a task uncancellable. Broad exception handlers should not accidentally defeat shutdown.

Q40. What must you do if you catch CancelledError explicitly? Re-raise it. Catching it for cleanup and not re-raising makes the task uncancellable and turns your shutdown deadline into a hang.

Q41. Can a CPU-bound task be cancelled? No. Cancellation is cooperative — the exception is delivered at a suspension point. A tight loop with no await never yields, so it cannot be cancelled.

Q42. gather(boom(), slow()) where boom raises immediately — what happens to slow? It keeps running, orphaned. gather propagates the first exception to the awaiter but does not cancel siblings. That is a resource leak, not a style difference.

Q43. How does TaskGroup differ? A failing child cancels the remaining children, and the group raises an ExceptionGroup on __aexit__. No task outlives its scope — structured concurrency.

Q44. How do you catch an ExceptionGroup? except* ValueError as eg: — the star form, Python 3.11+. eg.exceptions holds the matching ones.

Q45. When is gather(..., return_exceptions=True) correct? When you genuinely want all results including failures and no cancellation — a health-check fan-out, for example. It returns exceptions as values instead of raising.

Q46. What happens when a coroutine calls time.sleep(2)? The entire loop thread blocks for 2 seconds: no other coroutine runs, no fd is polled, no timer fires. Every pending task's latency grows by 2 s. Measured: a 10 ms ticker's largest gap goes from ~10 ms to ~162 ms.

Q47. Correct escape hatches for blocking work? await asyncio.to_thread(fn, ...) for blocking I/O (it releases the GIL), or loop.run_in_executor(ProcessPoolExecutor(), fn, ...) for CPU-bound work.

Q48. Why does a thread work for blocking I/O despite the GIL? Because the GIL is released around blocking I/O syscalls, so the OS-level wait happens outside the lock and other threads run.

Q49. Why do async generators need aclosing? Their cleanup must await, so it cannot run during garbage collection — there may be no running loop. Without aclosing, finally runs at loop.shutdown_asyncgens(), potentially much later, which under load is a connection leak.

Q50. Are asyncio.Lock and friends thread-safe? No. They are designed for a single event loop and are not safe across threads. Use threading.Lock for threads, and never mix without a documented bridge.

Q51. What is asyncio.Queue's maxsize for? Backpressure. A bounded queue makes the producer block when full, propagating slowness upstream instead of growing memory without bound.

Q52. How do you implement a timeout? async with asyncio.timeout(5): (3.11+) or await asyncio.wait_for(coro, 5). Both work by cancelling the inner operation, so the inner code must handle cancellation correctly.

Q53. What does asyncio.shield do? Protects an awaitable from cancellation propagating inward — the outer await can be cancelled while the inner operation continues. Use sparingly; it deliberately breaks the cancellation chain.

Q54. What is loop.call_soon_threadsafe for? Scheduling a callback onto the loop from a different thread. It is the only loop method safe to call from outside the loop's thread.

Q55. Why might await asyncio.sleep(0) be useful? It yields control to the loop without waiting, letting other ready callbacks run. Useful to break up a long CPU section — though the real fix is to move that work off the loop.

Q56. What is PYTHONASYNCIODEBUG=1 good for? Debug mode logs coroutines that were never awaited and callbacks that took too long — the fastest way to find a blocking call in a handler.

Q57. Graceful shutdown of a worker pool: sentinel or cancellation? Both. Sentinels (one per worker) drain queued work; cancellation is the hard deadline for workers stuck on I/O. Sentinel first, wait with a timeout, then cancel.

Q58. What happens if __aexit__ raises during cancellation? It replaces the in-flight exception, and you can lose the CancelledError. Cleanup code in __aexit__ should be defensive and should not swallow.

Q59. Why is async for over a network stream a backpressure mechanism? Because the consumer drives — the next chunk is only requested when the consumer is ready, so the TCP window closes naturally when the consumer falls behind.

Q60. What is the "async all the way down" problem? An async function can only await other async functions. Introducing one async call at the leaf forces every caller up the stack to become async, or to bridge through a thread. This is the main practical cost of adopting asyncio in an existing codebase.


Section 3: The GIL and Concurrency (Q61–Q85)

Q61. What is the GIL? A mutex allowing only one thread to execute CPython bytecode at a time.

Q62. Why does it exist? CPython's memory management is not thread-safe — every object has a refcount mutated constantly. Making refcounting atomic would be slow for the common single-threaded case; one global lock was simpler and faster.

Q63. What does the GIL guarantee? That one thread runs bytecode at a time, so individual bytecodes and C operations that never release it are effectively atomic.

Q64. What does it not guarantee? That any multi-bytecode sequence of yours is atomic. It protects interpreter internals, not your invariants.

Q65. Is counter += 1 thread-safe? No. It compiles to LOAD / ADD / STORE and a thread switch between them loses an update.

Q66. Is list.append(x) thread-safe? Yes. It is one C call that does not release the GIL mid-way.

Q67. Why does the classic lost-update demo often lose nothing on modern CPython? Since 3.10 the eval breaker — the flag handing the GIL to another thread — is checked only at specific instructions, mainly backward jumps and calls, not between every bytecode. In a tight loop the check lands on JUMP_BACKWARD, after the STORE, so the triple is uninterrupted by coincidence of code shape.

Q68. How do you make it reproduce? Put a call between the load and the store — counter = add_one(counter) — or use an operand whose __add__ is written in Python. Measured on the same machine: 3.2% and 60.9% of updates lost respectively, versus 0% for the bare +=.

Q69. What is the lesson from Q67–Q68? "I ran it and it didn't lose anything" is not evidence of atomicity. Whether a read-modify-write is interrupted depends on interpreter version, code shape, and operand type — none of which is a contract. If you need atomicity, take a lock.

Q70. When is the GIL released? Around blocking I/O syscalls, and inside many C extensions (numpy, compression, crypto) that explicitly release it around long computations.

Q71. Current status of free-threaded Python? PEP 703 designed it; PEP 779 defined the supported criteria. Phase II — officially supported but still not the default — landed in Python 3.14 (October 2025). Phase III (default) is not scheduled near-term.

Q72. What does the free-threaded build cost? Reported at 3.14: ~5–10% single-threaded overhead (down from ~40% at 3.13), ~15–20% more memory, and roughly 4× on suitable multi-threaded CPU-bound work.

Q73. Does removing the GIL fix your data races? No — arguably it makes them more likely to manifest, because true parallelism widens the interleaving window. It removes a global lock, not your missing locks.

Q74. How do you check at runtime whether the GIL is enabled? sys._is_gil_enabled() (available from 3.13).

Q75. Threads, processes, or asyncio — give the decision rule. Async for waiting (many concurrent I/O waits), processes for computing (CPU-bound), threads for when the library gives you no async option.

Q76. What does a thread cost? Roughly 8 MB of virtual stack each by default, plus context-switch overhead, plus every shared-state hazard. Thousands of threads is a memory and scheduling problem; thousands of coroutines is not.

Q77. What does a process cost? Serialization on every call (arguments and results are pickled), memory duplication, slow startup, and no shared objects without explicit shared memory.

Q78. What is multiprocessing's fork vs spawn difference? fork copies the parent's memory (fast, but unsafe with threads and with some libraries); spawn starts a fresh interpreter and re-imports (slower, safer, the default on macOS and Windows).

Q79. Why is fork dangerous in a threaded program? Only the forking thread survives in the child. A lock held by another thread at fork time is held forever in the child, and the child deadlocks on it.

Q80. What is a threading.local? Per-thread storage: each thread sees its own value for the same attribute. Useful for connections or request context without passing them explicitly.

Q81. Is dict thread-safe? Individual operations are atomic under the GIL, but compound ones are not — if k not in d: d[k] = v is a race. dict.setdefault is atomic and is the fix.

Q82. What is the difference between Lock and RLock? RLock can be acquired multiple times by the same thread (it counts), so recursive code does not self-deadlock. Lock cannot.

Q83. What does concurrent.futures give you over raw threads? A uniform Executor API over threads and processes, futures with results and exceptions, and map. Swapping ThreadPoolExecutor for ProcessPoolExecutor is a one-line change.

Q84. Why can a ProcessPoolExecutor deadlock? If a submitted callable is not picklable, or if a worker dies, or if you submit from within a worker. Also if the queue fills while all workers are blocked submitting.

Q85. What is the GIL's effect on tail latency? A CPU-bound thread holds the GIL for up to the switch interval (5 ms by default), so an I/O thread that becomes ready may wait that long. sys.setswitchinterval tunes it, trading throughput for responsiveness.


Section 4: Memory and the Object Model (Q86–Q115)

Q86. What are the two fields at the head of every PyObject? ob_refcnt (the reference count) and ob_type (a pointer to the type object).

Q87. Why is a Python list of a million ints so much bigger than a C array? Every int is a heap-allocated object with a header, and the list stores pointers to them. You pay for a million headers plus a million pointers. This is why numpy exists.

Q88. Explain a = [1,2,3]; b = a; b.append(4) — why does a change? Names are bindings, not boxes. a and b refer to the same object; assignment never copies.

Q89. Why does a mutable default argument persist across calls? The default is evaluated once when the def executes and stored on the function object (f.__defaults__). It is not re-evaluated per call.

Q90. Why does sys.getrefcount(x) return one more than you expect? Passing x as an argument creates a temporary reference.

Q91. Two properties of refcounting that make it a real tradeoff? Pro: deterministic, prompt deallocation with no pause times. Con: cannot collect cycles, and every reference operation touches memory, hurting locality — plus it must be atomic under free-threading.

Q92. Why does CPython need a cycle collector at all? Refcounting cannot free mutually-referencing objects: each keeps the other's count above zero.

Q93. How does the generational collector decide what is garbage? Within a generation, it subtracts references originating inside the generation. Objects with a nonzero remainder are reachable from outside and live; the rest are garbage.

Q94. What are the default GC thresholds and what do they mean? The shape is (gen0, gen1, gen2): gen 0 runs after that many more allocations than deallocations; gen 1 after that many gen-0 collections; gen 2 after that many gen-1 collections. The value is version-dependent — long documented as (700, 10, 10), it is (2000, 10, 10) on CPython 3.13, which also introduced an incremental collector. The correct answer is to say the shape and then gc.get_threshold(), not to recite a number. This question is in the bank specifically as a reminder that reciting a memorized constant is how you get corrected in an interview.

Q95. Which objects are GC-tracked? Only containers — things that can reference other objects. An int or str can never participate in a cycle and is never tracked.

Q96. What does gc.freeze() do and when is it used? Moves all current objects to a permanent generation the collector ignores. Called before forking in a pre-fork server so the child's GC does not touch (and copy-on-write-fault) shared parent pages.

Q97. Does __del__ run for objects in a cycle? Yes, since Python 3.4 / PEP 442. Before that they were uncollectable and went to gc.garbage.

Q98. Name three remaining hazards of __del__. Undefined finalization order within a cycle (peers may already be finalized); exceptions inside it are swallowed and printed to stderr; the object can be resurrected. It also may not run at interpreter shutdown.

Q99. What should you use instead of __del__? A context manager for scoped resources, or weakref.finalize for lifetime-tied cleanup — and in the finalize callback, do not capture self, or you keep the object alive forever.

Q100. Describe CPython's allocator hierarchy. Arenas (256 KB from mmap) contain pools (4 KB, each dedicated to one size class) containing blocks (fixed-size slots). Allocations ≤ 512 bytes use pymalloc; larger go to malloc.

Q101. "I freed everything and RSS didn't drop." Is that a leak? Usually not. An arena is released only when every pool in it is empty, so one long-lived object can pin 256 KB. Fragmentation, not leakage.

Q102. What does __slots__ actually remove? The per-instance __dict__, replacing hash-table attribute storage with fixed offsets in the object struct.

Q103. How much does __slots__ save, measured? About 38% over 200,000 three-attribute instances (19.9 → 12.2 MiB) in the measurement in this track. Always measure — key-sharing dicts (PEP 412) already reduced the gap.

Q104. What does __slots__ break? Adding attributes not in the list; weak references unless you add '__weakref__'; multiple inheritance from two classes with non-empty slots.

Q105. What happens if a subclass of a slotted class omits __slots__? It regains a __dict__ and most of the saving evaporates. Every class in the hierarchy must declare it; __slots__ = () is the way to add nothing.

Q106. Why is sys.getsizeof misleading? It measures only the object's own footprint, not what it references. A list of 50,000 strings reports 434 KiB while actually costing ~3,510 KiB.

Q107. What do you use instead? tracemalloc to attribute real allocations to source lines, or pympler.asizeof for a deep size. And remember neither equals RSS, because of arena behaviour.

Q108. What does memoryview give you? A zero-copy view over any object supporting the buffer protocol. Slicing a memoryview allocates nothing; slicing bytes copies. Measured: 16 MiB copy vs ~0 for the view.

Q109. Name three stdlib things that use the buffer protocol. socket.recv_into, struct.unpack_from, array, mmap, and numpy arrays — all avoid copies by writing into or reading from an existing buffer.

Q110. a=256; b=256; a is b? c=257; d=257; c is d? int("257") is int("257")? True, True, False. Small ints (−5 to 256) are cached singletons; 257 is constant-folded to one object within a single code object; the third is computed at runtime so they are distinct objects.

Q111. What is the rule about is? Never use it for value comparison. Use it for identity only — x is None, sentinel checks. Whether equal values are the same object depends on the compiler, which is not part of the language.

Q112. What is string interning and when does it happen automatically? Storing one canonical copy of a string. CPython automatically interns short identifier-like strings (compile-time constants, names). sys.intern does it explicitly, which is worth it when you hold millions of repeated strings.

Q113. Why can't you weakref an int or a tuple? They lack a __weakref__ slot. Built-in immutable types generally do not support weak references; user classes do by default unless they define __slots__ without it.

Q114. What is a WeakValueDictionary for? A cache that does not keep its values alive — entries disappear when the value is collected elsewhere. The classic way to build an object registry without leaking.

Q115. Why does a long-running Python service fragment? Freed blocks return to their pool, and a pool's arena is released only when fully empty. A workload that allocates many objects of one size class and then a few of another can leave arenas pinned by a handful of survivors.


Section 5: The Data Model (Q116–Q140)

Q116. Rank the attribute lookup order for obj.x. Data descriptor on the type → instance __dict__ → non-data descriptor on the type → class attributes up the MRO → __getattr__.

Q117. What makes a descriptor a data descriptor? It defines __set__ or __delete__ in addition to __get__. That places it ahead of the instance dict.

Q118. Why can't you shadow a @property with an instance attribute? property is a data descriptor, so it outranks the instance dict. Assigning to a property without a setter raises AttributeError.

Q119. Why can you monkeypatch a method on an instance? A plain function is a non-data descriptor, so the instance dict outranks it.

Q120. How does self get bound? A function stored on a class is a non-data descriptor whose __get__ returns a bound method — a partial application of the function to the instance. self is the descriptor protocol, not magic.

Q121. When is __getattribute__ called? For every attribute access, unconditionally.

Q122. When is __getattr__ called? Only as a fallback, when normal lookup raised AttributeError.

Q123. Which is the performance hazard, and why? __getattribute__, because it intercepts every access including self.anything inside your own methods. __getattr__ costs nothing on hits.

Q124. What is the classic __getattribute__ bug? Touching self.__dict__ inside it re-enters __getattribute__ to fetch __dict__RecursionError. Delegate to object.__getattribute__(self, name).

Q125. What does super() actually resolve to? The next class in the MRO of type(self) — which depends on the instance, not the definition site.

Q126. In a diamond Diamond(Left, Right) where both derive from Base, what does Left.go's super() reach? Right, not Base — because the MRO is computed from Diamond: Diamond → Left → Right → Base → object.

Q127. What algorithm computes the MRO? C3 linearization. It guarantees a class precedes its bases, preserves declaration order among bases, and is monotonic. If no consistent linearization exists, the class statement raises TypeError.

Q128. What breaks cooperative multiple inheritance? One class calling Base.method(self) directly instead of super().method() — it skips everything after it in the MRO.

Q129. What does returning True from __exit__ do? Suppresses the exception raised in the with block. Returning None lets it propagate. A bare return True silently eats every bug in the block.

Q130. What arguments does __exit__ receive on a clean exit? (None, None, None).

Q131. What is contextlib.contextmanager doing under the hood? Wrapping a generator: everything before yield is __enter__, the yielded value is the as target, and everything after — including the finally — is __exit__. Exceptions are thrown into the generator at the yield.

Q132. What is __slots__' interaction with @property? They coexist, but a slot and a property of the same name conflict — the class body's property overwrites the slot descriptor. Name them differently (_x slot, x property).

Q133. What does __init_subclass__ do? A hook called on the parent whenever a subclass is defined. A lighter alternative to a metaclass for registration or validation.

Q134. What is a metaclass, in one sentence? The class of a class — it controls class creation, so it runs once at definition time rather than per instance.

Q135. When do you actually need a metaclass? Almost never. __init_subclass__ and __set_name__ cover registration and descriptor naming; decorators cover most of the rest. Reach for a metaclass only when you must alter the class namespace during creation.

Q136. What does __set_name__ do? Called on a descriptor when the owning class is created, telling it the attribute name it was assigned to. It is how a descriptor learns its own name without repetition.

Q137. Difference between __str__ and __repr__? __repr__ is for developers and should be unambiguous (ideally eval-able); __str__ is for users. str() falls back to __repr__ if __str__ is absent, not the reverse.

Q138. What must be true of __hash__ and __eq__ together? Equal objects must have equal hashes. Defining __eq__ without __hash__ sets __hash__ to None, making instances unhashable — deliberately, because a mutable-equality object is a broken dict key.

Q139. What does functools.total_ordering do? Fills in the remaining comparison methods from __eq__ plus one of __lt__/__le__/__gt__/ __ge__. Convenient, slightly slower than writing them.

Q140. What is __call__ for? Making an instance callable. It is how decorators-with-state, and any object that wants to look like a function, are built.


Section 6: Performance and Idiom (Q141–Q150)

Q141. What does functools.lru_cache key on? The argument tuple — so positional and keyword forms are different keys: f(1, 2) and f(1, b=2) miss each other. Unhashable arguments raise TypeError.

Q142. What is the lru_cache-on-a-method trap? It keys on self, so the cache holds a strong reference to every instance it has ever seen — an unbounded leak on a long-lived class. Use functools.cached_property, a per-instance cache, or key on an id you control.

Q143. When does dis help? When you need to prove what the interpreter actually does — that += is three instructions, or that a comprehension builds a list. It settles arguments that intuition gets wrong.

Q144. Why is string concatenation in a loop slow, and what is the fix? Strings are immutable, so each += allocates a new string and copies — O(n²) overall. Build a list and "".join(parts), which is O(n). (CPython has an in-place optimization for the simple case, but it is fragile and not something to rely on.)

Q145. When is a deque better than a list? Any time you pop or append at the front: list.pop(0) is O(n) because it shifts everything; deque.popleft() is O(1).

Q146. What does __slots__ do for speed, not just memory? Attribute access becomes a fixed offset instead of a dict lookup, so it is modestly faster — but the memory win is usually the reason to do it.

Q147. When should you reach for a C extension or Cython? After profiling shows a tight numeric or per-byte loop dominating. Before that, the answer is usually a better algorithm, a batch API (str.find instead of a per-character loop), or numpy.

Q148. What is the fastest way to process a 40 GB file? Stream it: iterate the file object (which reads in buffered chunks) or read(chunk) in a loop, and never materialize it. Combine with memoryview if you need to slice binary records without copying.

Q149. How do you profile a Python service properly? cProfile for deterministic function-level profiling in development; a sampling profiler (py-spy, austin) in production because it does not require restarting or instrumenting; tracemalloc for memory attribution.

Q150. What is the single most common Python performance mistake in production services? A blocking call inside an async handler — one synchronous library call in a request path, and the whole service's tail latency collapses under load. See Q46.


Scoring

CorrectLevel
0–59L0 — foundations missing
60–99L1 — outcomes known, mechanisms not
100–129L2 — mechanisms known
130–150L3 — mechanisms plus version-dependent facts, and reaches for a measurement

Confident-wrong modifier: 2–3 → −0.5 level. 4+ → −1 full level, and every one enters the review queue at the 1-day interval.

Section weighting for planning (not for the score): Sections 1 and 2 — iterators/generators and async — are what reportedly surfaces in the loop. Six of eight on iterators with weak memory answers is a very different study plan from the reverse.