Question presented to candidate:
"You never call free() in Python, yet memory does get released. Explain how CPython manages memory: where objects live, what decides when one is freed, why reference counting alone is not enough, and what the gc module actually does."
What a strong answer should cover:
- Every object lives on a private heap managed by the interpreter; names and containers hold references, never the objects themselves.
- Reference counting is the primary mechanism: each object has a count, and CPython frees it the moment the count drops to zero (deterministic, immediate, no pause).
- Reference cycles never reach zero, so a separate cyclic garbage collector (the
gcmodule) periodically finds unreachable groups of container objects; it is generational (three generations, default thresholds(700, 10, 10)on 3.11 and 3.12) and only tracks containers. - The allocator: pymalloc serves small requests (up to 512 bytes) from pools inside arenas, larger ones go to the system
malloc, so freed memory is reused by Python but not always returned to the OS. - Practical tools and gotchas:
sys.getrefcount,sys.getsizeof(shallow),tracemalloc,gc.collect(),gc.disable()/gc.freeze(); immortal objects in 3.12 (PEP 683); this is CPython behaviour, PyPy uses a different collector.
Clarifying questions expected:
- Do you mean CPython specifically, or Python implementations in general (PyPy, the free-threaded build)?
- Is this about understanding the model, or about a concrete problem such as a memory leak or high RSS in a long-running service?
Code / implementation expected: Optional โ sys.getrefcount deltas and a two-object cycle that only gc.collect() frees are the classic demonstration.