Memory & GC
Python never gives you an object directly — a name, a list element or an attribute is a reference, and the object itself lives on the heap. Memory management therefore reduces to two questions: how many references to the object still exist, and is it reachable from outside at all. CPython answers them with two independent mechanisms, and almost every mistake in this topic is a confusion of their responsibilities. The first, reference counting, runs continuously and frees an object the moment its last reference disappears. The second, the cyclic collector in the gc module, runs periodically and covers the single case the first one cannot — mutual references.
There is a third layer people forget about most often — the allocator. Destroying an object does not mean the memory went back to the operating system: pymalloc keeps freed blocks in its own pools and hands the OS only a fully emptied arena. Hence the most expensive myth of the topic — "process RSS is not going down, so there is a leak". It does not follow. And one more caveat that senior interviews probe separately: all of the above are properties of CPython, not of the language. The specification promises neither reference counting nor a moment of destruction; PyPy has no reference counting at all, and code that relies on "the file closes itself as soon as the name goes out of scope" silently breaks there.
Topic map
- Reference counting — the counter in every object header, cascading deallocation at zero, and why the freed memory still stays with the process.
- Shallow and deep copies — assignment copies nothing,
copy.copycopies one level,copy.deepcopywalks the whole graph with cycle protection. - The cyclic collector — why reference counting is blind to mutual references and how
gcproves unreachability by subtracting internal references. - Generations and tuning
gc— three generations and what the 700 threshold really counts,gc.disable()andgc.collect()for bulk loads,gc.freeze()beforefork.
Common mistakes and traps
| Mistake | Consequence |
|---|---|
| Thinking only the periodic collector frees memory | The primary mechanism is missed — reference counting frees an object immediately, while gc deals only with cycles |
Reading the number from sys.getrefcount literally | It is always one too high because of the argument itself, and in CPython 3.12+ None, True and small ints are immortal, so their count never moves |
Expecting del x to shrink the process footprint | del only removes a name; the object's block returns to a pymalloc pool, not to the operating system |
| Equating an RSS that never shrinks with a leak | An arena goes back to the OS only when completely empty — far more often this is fragmentation, and the two are told apart by the count of live objects |
Believing original[:] or list(original) produce an independent copy | That is exactly a shallow copy — nested objects are shared, and mutating a nested one is visible through both |
Thinking gc.disable() stops memory reclamation | It stops only the cyclic collector; reference counting is not disabled and cannot be |
| Assuming older generations are scanned more often than younger ones | The opposite — the older the generation, the rarer the pass, because a survivor is statistically going to live long |
Measuring a structure's size with sys.getsizeof | The function is shallow and accounts only for the object itself — a list holding two large nested lists reports 72 bytes |
What interviews check
The topic is unrolled as a ladder, and the first rung is almost always the same — "how is memory freed in Python". A correct answer opens with "reference counting first, and it frees immediately", while "a garbage collector comes around periodically" instantly turns the conversation into a review of your mistakes. Next comes the near-guaranteed copy versus deepcopy pair, often as a practical task where a copy was taken before the mutation and the original changed anyway. What is wanted here is not a memorised definition but an explanation in terms of references: the outer container is new, the nested objects are the very same ones.
At middle and senior level the questions turn mechanical. Why is gc needed if reference counting exists — the answer is cycles. How does the collector tell an unreachable cycle from a live one — the answer is subtraction of internal references. What exactly does the gen 0 threshold count — not time and not the number of objects on the heap, but the difference between allocated and deallocated tracked containers. Can anything be turned off — reference counting cannot, the cyclic collector can and sometimes should be. And the closing senior question about RSS that never returns to the system. The typical failure across this block is naming gc as the only mechanism or declaring any memory growth a leak; a strong answer, by contrast, states the CPython version on its own initiative, because thresholds and arena sizes are implementation details, not language guarantees.