skip to content

Memory Model

How CPython decides an object is dead and takes its memory back: reference counts first, a generational cycle collector behind them, weak references when you must not keep something alive.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

23

Why can reference counting alone never free two Python objects that point at each other?

level: juniorimportance: must knowfreq 55%

answer

  1. Counting alone has a blind spot
  2. Each keeps the other alive
  3. Neither count ever reaches zero
  4. A second, tracing collector exists
  5. gc module, container objects only

basics

~20 s

Each object in the cycle holds a reference to the other, so neither count drops to zero even after every outside name is gone. CPython runs a second, tracing collector — the gc module — to find and free such groups.

solid answer

~40 s

CPython frees an object the instant its reference count reaches zero. Two objects that reference each other keep each other's count at one forever, so once the last outside name disappears the pair is unreachable but immortal under counting alone. CPython therefore runs a tracing cycle collector alongside the counts, exposed as the `gc` module. It looks only at container objects — things that can hold references — and finds groups whose references come entirely from inside the group, which means nothing live can reach them. Those are freed together. The practical consequence is that cyclic objects die whenever the collector next runs, not at a predictable moment, so a file handle or socket must be released by a context manager or an explicit close rather than by finalization.

code

python · 13 lines
python
import gc

class Node:
    def __init__(self):
        self.peer = None

a = Node()
b = Node()
a.peer = b
b.peer = a
del a, b          # every outside name is gone; both counts are still 1

print(gc.collect())   # number of unreachable objects the collector found

go deeper

for a junior

Be ready to say that CPython frees an object when its reference count hits zero, and that two objects pointing at each other never reach zero. Naming the gc module as the second, tracing mechanism is enough at this level.

for a middle

Explain which objects the collector tracks — containers, never integers or strings — and how it decides a group is unreachable by discounting the references the group makes to itself. Be able to sketch a cycle in five lines of code.

for a senior

Show that cycle collection is non-deterministic, so resources are released by context managers rather than finalizers, and that the return value of gc.collect() in a steady-state loop is a usable production signal.

for a principal

Own the consequence: cycle-heavy object graphs are a design choice with a memory and latency cost, and code that depends on refcount timing is CPython-only. Say that out loud before a team builds a resource-release strategy on it.

**The problem in one sentence.** CPython frees an object the moment its reference count reaches zero. Two objects that hold references to one another keep each other's count above zero forever, so that rule alone can never reclaim them. **What a cycle looks like.** ```python class Node: def __init__(self): self.peer = None a = Node() b = Node() a.peer = b b.peer = a del a, b ``` After `del a, b` no name in the program can reach either object, yet each still has exactly one reference — the attribute stored on the other. The counts sit at one and never fall. Nothing outside the pair can drop them, and the pair will not drop itself. Cycles are not exotic. Parent/child back-pointers, doubly linked structures, a callback stored on the very object it will call, a closure that captures a name bound to the object holding the closure, and a caught exception whose traceback keeps the frame that holds the exception all form cycles. Any object graph with a back-edge is one. **The second collector.** CPython therefore runs a tracing cycle collector alongside reference counting, exposed as the `gc` module. It supervises only *container* objects — objects that can hold references to other objects. `gc.is_tracked(x)` reports whether a given object is under its supervision: an `int`, a `float` or a `str` cannot reference anything and is never tracked, and CPython also untracks tuples once it has observed that they contain only atomic values. For the tracked set the collector answers one question: which of these objects are referenced *only* from inside the set? It walks the tracked objects and discounts the references they make to one another; anything left with no reference from outside cannot be reached from a live root, so it is garbage even though its real count is positive. Those objects are finalized and freed as a group. **Consequences that matter in an interview.** 1. *Collection is not deterministic.* A non-cyclic object dies the instant its last name goes away. A cyclic one dies whenever the collector next runs — possibly thousands of allocations later, possibly never if automatic collection was turned off. Never rely on finalization to release a file handle, a socket or a lock: use a context manager or an explicit `close()`. 2. *`gc.collect()` returns a number.* It runs a full collection immediately and returns how many unreachable objects it found. A steadily non-zero number in a steady-state loop is a direct signal that the code manufactures cycles. 3. *A `__del__` method inside a cycle is no longer fatal.* Before Python 3.4 a cycle containing an object with `__del__` was declared uncollectable and parked in `gc.garbage` for the life of the process, because the interpreter could not pick a safe finalization order. PEP 442 changed that in 3.4: finalizers now run once, in unspecified order, and the cycle is then freed. On 3.14 `gc.garbage` stays empty in normal operation. 4. *This is a CPython implementation detail.* Counting plus a cycle collector is how CPython works; other Python implementations trace everything and keep no per-object count, so code that happens to work because a count hit zero at the right moment is not portable. **Breaking a cycle deliberately.** You can clear the back-edge yourself when you are finished with a structure, or keep the back-pointer as a non-owning reference so it never contributes to the count at all. Both are ordinary design decisions rather than a fight with the collector. **Two things this is not.** A circular *import* is not a memory problem: the modules involved stay in `sys.modules` for the life of the process, so they remain reachable and the collector never has an opinion about them — a circular import is a startup-ordering bug, not a leak. And an object graph still reachable from a module-level cache or a registry is not garbage at all; no collector will free it. Memory that `gc.collect()` refuses to shrink is a live-reference problem, not a cycle problem, and chasing it with the `gc` module's knobs is wasted effort.

  • Does a circular import between two Python modules create garbage the collector has to clean up?
    No. Both modules stay registered in `sys.modules` for the life of the process, so they are permanently reachable from a live root and are never garbage. A circular import is an execution-order bug — a name is read before the partially initialised module has defined it — and the fix is restructuring the imports, not the `gc` module.
  • What happens on Python 3.14 to a cycle whose objects define `__del__`?
    It is collected. Each finalizer runs exactly once, in an order the interpreter does not promise, and the cycle is then freed. That has been true since PEP 442 in Python 3.4; before it, such cycles were declared uncollectable and appended to `gc.garbage` forever, which is where the old advice to avoid `__del__` entirely comes from.
  • If cycles are collected anyway, why should anyone care that they exist?
    Because the timing changes. Non-cyclic objects are freed at the instant of the last decref; cyclic ones wait for a collection pass, so memory sits high in between and any resource release attached to finalization is delayed by an unbounded amount. In allocation-heavy code, cycles also make the collector run more often, which costs CPU.

Two climbers each clipped only to the other: counting the ropes attached to each one never reaches zero, so someone has to look at the whole rope system from outside and notice the pair is anchored to nothing.

saying these in an interview costs you the question

  • Says Python has no garbage collector, only reference counting
  • Claims `del` frees the object rather than removing a name
  • Thinks a cycle leaks permanently on modern Python
  • Believes `__del__` in a cycle still makes it uncollectable
  • Relies on finalization to close files or sockets
  • Calls a circular import the same thing as a reference cycle

context

open as a page

In CPython, what does `del x` actually do — does it free the object?

level: juniorimportance: must knowfreq 62%

basics

~20 s

del x only unbinds the name x from its namespace and drops one reference to the object. CPython deallocates the object at the moment its reference count reaches zero, so if a list, attribute or another name still holds it, nothing is freed.

open as a page

What does weakref.ref give you that an ordinary Python reference does not?

level: juniorimportance: must knowfreq 40%

basics

~10 s

A weakref.ref points at an object without counting toward the references that keep it alive. Call the ref like a function to get the object back, or None once the object has been collected.

open as a page

How much memory does adding __slots__ to a Python class actually save?

level: middleimportance: must knowfreq 60%

basics

~20 s

__slots__ fixes the attribute names up front so instances store values at known offsets and carry no per-instance dictionary. On CPython 3.14 that is roughly a 35-45% cut for a small class - real, but far less than the old folklore.

open as a page

A Python inventory-sync worker's RSS climbs each batch and never falls: how do you tell a real leak from allocator retention?

level: seniorimportance: must knowfreq 50%

basics

~20 s

Log sys.getallocatedblocks() once per batch. A live block count that returns to baseline while RSS does not means the objects are gone and pymalloc is holding fragmented arenas; a climbing count is a real leak, localised with tracemalloc.

open as a page

Does deleting a large Python list return its memory to the operating system?

level: juniorimportance: should knowfreq 40%

basics

~20 s

Usually not right away. CPython deallocates the objects immediately, but its small-object allocator keeps the freed blocks for reuse and hands a 1 MiB arena back to the OS only once that whole arena is empty.

open as a page

Why does a Python instance with three attributes cost far more memory than three raw values?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Every Python object starts with a header - a reference count and a pointer to its type - and a normal instance carries attribute storage on top of that. The values are separate objects too, each with its own header.

open as a page

How does CPython's pymalloc allocator handle small objects differently from malloc?

level: middleimportance: should knowfreq 35%

basics

~20 s

pymalloc groups requests of 512 bytes or less into size-class pools carved from 1 MiB arenas, so most Python objects are served by a free-list pop instead of a system call path. Larger requests fall through to malloc.

open as a page

When does CPython's cycle collector actually run, and what do the gc thresholds control?

level: middleimportance: should knowfreq 45%

basics

~20 s

It is driven by allocation pressure, not by memory use or a timer. CPython counts tracked container allocations minus deallocations; when that surplus passes the first threshold from gc.get_threshold(), a young-generation pass runs and survivors are promoted.

open as a page

What does sys.intern() do to a Python string, and when is calling it worth it?

level: middleimportance: should knowfreq 40%

basics

~20 s

sys.intern() stores one canonical copy of a string in the interpreter's intern table and returns it, so equal strings built at runtime collapse to a single object. It pays off when a few distinct values repeat many times.

open as a page

Why does sys.getsizeof on a list of dicts under-report the memory it really uses?

level: middleimportance: should knowfreq 52%

basics

~10 s

sys.getsizeof is shallow: it reports only that object's own storage plus collector bookkeeping and never follows a pointer. For a list it counts the pointer array, not the dictionaries the pointers lead to.

open as a page

Why does `sys.getrefcount(x)` report one more reference than your code holds?

level: middleimportance: should knowfreq 40%

basics

~20 s

Passing x into sys.getrefcount creates one more reference — the argument bound to the function's parameter — and that reference is alive while the count is read. A module-level name that is the only holder therefore reads 2, not 1.

open as a page

Why prefer weakref.finalize over __del__ for releasing an object's resource?

level: middleimportance: should knowfreq 26%

basics

~20 s

weakref.finalize registers a callback plus its arguments separately from the object, so it runs at most once, cannot resurrect the object, and still fires at interpreter exit. del lives on the object, swallows its own exceptions, and can be skipped.

open as a page

What is the difference between weakref.WeakValueDictionary and WeakKeyDictionary?

level: middleimportance: should knowfreq 34%

basics

~10 s

WeakValueDictionary holds its values weakly, so an entry disappears when the last strong reference to that value goes. WeakKeyDictionary holds its keys weakly, so an entry disappears when the key object dies.

open as a page

How do you confirm reference cycles are behind a Python transcript archiver's memory growth across 6,800-row batches?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Force a gc.collect() between batches and watch its return value and the object counts. If a forced pass reclaims a lot, the growth is cyclic garbage awaiting collection; if it reclaims nothing, the objects are still reachable and the collector is irrelevant.

open as a page

When is calling gc.disable() in a production Python service defensible, and what breaks?

level: seniorimportance: should knowfreq 30%

basics

~20 s

It stops automatic cycle collection only — reference counting keeps freeing everything acyclic — so it is defensible in short-lived or fork-heavy processes. What breaks is cyclic garbage: it accumulates without bound unless the code calls gc.collect() at safe points.

open as a page

How can sys.intern cut memory in a nightly report generator holding millions of parsed rows, and where does it stop helping?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Parsed fields are freshly allocated, so repeated keys and category values exist once per row. Interning them as the parser produces them leaves one object per distinct value. It does nothing for unique values, numbers or the row containers.

open as a page

A sensor-telemetry collector buffers millions of float readings in a list - how do you cut the memory per reading?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Stop making one Python object per reading. A list of floats costs about 32 bytes per value on 3.14 - a pointer plus a boxed float; array.array("d") stores the same doubles contiguously at about 8 bytes each.

open as a page

Why is relying on CPython's refcounting to close files unsafe on other implementations?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Freeing an object the instant its reference count hits zero is a CPython implementation detail, not a language guarantee. Implementations with tracing collectors, such as PyPy or GraalPy, finalize whenever they choose, so an unclosed file can stay open indefinitely.

open as a page

Why would a WeakValueDictionary cache in a billing run show a near-zero hit rate?

level: seniorimportance: should knowfreq 24%

basics

~20 s

Because nothing else holds the cached values. A WeakValueDictionary only retains an entry while some other strong reference exists, so if each record is used and dropped inside one loop iteration the entry is gone before the next lookup.

open as a page

Why does `a = 1000` then `b = 1000` make `a is b` True in a script but False in the REPL?

level: juniorimportance: nice to knowfreq 24%

basics

~20 s

A whole file compiles as one unit, and the compiler stores each distinct constant once, so both names get the same 1000 object. The REPL compiles each entered statement separately, so each line allocates its own.

open as a page

What does the PYTHONMALLOC environment variable control in CPython?

level: middleimportance: nice to knowfreq 15%

basics

~20 s

PYTHONMALLOC picks which allocator CPython installs and whether debug hooks wrap it: pymalloc by default, malloc to route every request to the C library, and the debug variants to add guard bytes and fill patterns.

open as a page

Why does `sys.getrefcount(None)` return a huge constant on CPython 3.14?

level: middleimportance: nice to knowfreq 16%

basics

~20 s

Since CPython 3.12, interpreter singletons such as None, True, False and the empty tuple are immortal: their reference count is pinned to a sentinel value in the billions and increments and decrements skip them entirely, so the number is not a count of anything.

open as a page