Backend / Python core / 24_memory_model.md

Python memory model — allocation, refcounting, GC

Updated 10 min read source
On this page10
  1. The object header
  2. Reference counting
  3. Finalizers: __del__ and weakref.finalize
  4. The cycle collector
  5. Inspecting an object’s references
  6. The allocator, in brief
  7. Where it breaks in production
  8. The trade not taken
  9. Related
  10. Interview angle

Python memory model — allocation, refcounting, GC

Article.

When is a Python object freed? CPython answers with two mechanisms in tandem: reference counting, which frees almost everything the moment the last reference goes, and a cycle collector for the objects reference counting cannot free on its own. Underneath both sits the allocator. Outputs below are from Python 3.14.5 unless noted.

Where frames and references live is in Stack and heap in Python.

The object header

Every object starts with the same two fields: a reference count and a pointer to its type. That is 16 bytes on a 64-bit build, the whole of sys.getsizeof(object()). Objects that can contain other objects (lists, dicts, instances of ordinary classes) carry a further 16-byte header for the cycle collector, which is why sys.getsizeof([]) is 56 and not 40.

Reference counting

The count goes up when a reference is created (an assignment, an argument, an insertion into a container) and down when one goes away (del, a name rebound, a function returning, a removal). At zero, the object is freed on the spot.

python
import sys

x = []
y = x
sys.getrefcount(x)   # 3 at module level: x, y, and the call's own argument

def local():
    a = []
    b = a
    return sys.getrefcount(a)

local()              # 2 on 3.14, 3 on 3.12 and 3.13

The exact number is version-dependent: 3.14 can borrow references instead of counting them for local variables, which cuts refcount traffic, so getrefcount reports one fewer. Never assert on its value.

Freeing is immediate and cascades

python
class Noisy:
    def __init__(self, n): self.n = n
    def __del__(self): print("freed", self.n)

n = Noisy(1)
del n                         # freed 1 - right here, before the next line
outer = [Noisy(2), [Noisy(3)]]
del outer                     # freed 3, freed 2: the list releases its items

When an object is freed, it drops the references it held, which can bring those objects to zero in turn. That is what makes CPython’s cleanup deterministic and why with is not strictly needed to close a file that goes out of scope.

Two exceptions to “immediately”: the free-threaded build defers counting for some objects (modules, top-level functions, methods defined in a class body), which are freed only at the next collection, and PyPy has no reference counting at all. Code that must release a resource at a known point uses with, never “it will be freed when it goes out of scope”.

Immortal objects

Since 3.12 (PEP 683) some objects never die: None, True, False, the small ints and other core singletons have a fixed reference count that is never updated.

sys.getrefcount(None) prints 4294967295 on 3.12 and 3.13 and 3221225472 on 3.14, and sys._is_immortal(None) (3.14) is True. Skipping the writes to their count avoids contention between threads and spares memory pages that would otherwise be touched on every use.

The cost of reference counting

Every reference copy is a write to an object’s header, which costs memory traffic and is why the GIL existed: on the default build those increments are plain, non-atomic operations, made safe because only one thread runs Python at a time.

The free-threaded build (official in 3.14, PEP 779) cannot do that, and uses biased reference counting instead: a cheap non-atomic count for the thread that owns the object plus an atomic shared count for everyone else, together with deferred and per-thread counting for the most heavily shared objects (The GIL, and free-threaded Python).

The one thing reference counting cannot do is free a cycle:

python
a = []
b = []
a.append(b)
b.append(a)
del a, b   # each still holds the other: refcounts stay at 1

That is the cycle collector’s job, below.

Finalizers: __del__ and weakref.finalize

__del__ runs when an object is about to be freed: at refcount zero, or when the cycle collector frees a cycle it is part of. Since 3.4 (PEP 442) objects with __del__ in cycles are collected too, and each finalizer runs at most once per object, even if __del__ resurrects it.

An exception raised in __del__ cannot propagate anywhere, so it goes to sys.unraisablehook, which prints it: Exception ignored while calling deallocator on 3.14, Exception ignored in before.

Still, avoid __del__: it may run at awkward times (inside an unrelated allocation that triggered a collection, or at interpreter shutdown when module globals are already gone). Prefer a context manager for deterministic cleanup, and weakref.finalize for “run this when the object dies”, which is registered explicitly and also runs at exit.

The cycle collector

Reference counting frees everything except cycles. The gc module’s collector finds those: it looks only at objects that can hold references (containers), and it has no list of roots to start from, so it works the other way round.

text
for each tracked container in the generation:
  gc_refs = its refcount
for each reference held by those containers:
  decrement the target's gc_refs        (internal refs)
gc_refs > 0  -> referenced from outside: reachable,
                and so is everything it reaches
the rest     -> only referenced from inside: garbage

What remains unreachable is handled in order: weak-reference callbacks, then finalizers (__del__, at most once per object since PEP 442), then the references inside the cycle are cleared so reference counting frees it.

python
import gc

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

a, b = Node(), Node()
a.other, b.other = b, a
del a, b
gc.collect()        # 2: the two nodes, invisible to refcounting

Generations and thresholds

Most objects die young, so new containers start in generation 0 and survivors move up. A collection of generation 0 runs when allocations minus deallocations of containers exceed the first threshold; the older generations run after a number of younger collections.

A full collection also waits until the long-lived objects have grown by about 25%, so on a big heap full collections are rarer than the thresholds suggest.

Version gc.get_threshold() Collector
3.12 (700, 10, 10) three generations
3.13 (2000, 10, 10) three generations
3.14.0 to 3.14.4 (2000, 10, 10) incremental: young and old
3.14.5+, 3.15 (2000, 10, 10) three generations again

The 3.14 detour is worth knowing. 3.14.0 shipped an incremental collector with two generations, which cut the longest pauses but let garbage pile up between collections; after reports of significant memory pressure in production, 3.14.5 (May 2026) went back to the 3.13 collector.

So “which GC does Python 3.14 have” depends on the patch release: check with python -c "import gc, sys; print(sys.version, gc.get_threshold())" rather than a blog post.

What is tracked

Only objects that can be part of a cycle are tracked: gc.is_tracked(1) is False. The details move between versions: on 3.14.5 a dict is tracked from creation (gc.is_tracked({"a": 1}) is True; on 3.12 it was untracked until it held a container), and a tuple of atomic values is never tracked, where 3.12 tracked it until a collection untracked it.

The free-threaded build

The free-threaded build has its own collector, separate from the generational one: it stops all threads while it runs, and it is also what frees the deferred-refcount objects and merges per-thread reference counts. Its pause behaviour differs, so measure on the build you deploy.

What a collection costs

python
import time

def timer(phase, info):
    if phase == "start":
        timer.t = time.perf_counter()
    else:
        ms = (time.perf_counter() - timer.t) * 1000
        print(info["generation"], f"{ms:.0f} ms", info["collected"])

gc.callbacks.append(timer)

On 3.14.5 a full collection over a million live cyclic objects took about 50 ms, and freeing them took 135 ms, all while the program is paused. The callback above is the production version of that measurement: export the pause per generation as a histogram and correlate it with latency spikes. gc.get_stats() gives counts per generation.

Inspecting an object’s references

python
import gc

x = SomeObject()
gc.get_referrers(x)   # list of objects that reference x
gc.get_referents(x)   # list of objects x references

Useful for memory leak hunting — find what’s holding onto an object that should have been freed.

The allocator, in brief

Freed memory goes back to CPython’s small-object allocator, pymalloc, not straight to the OS: objects of 512 bytes or less live in 16 KiB pools inside 1 MiB arenas, and an arena is returned only when every pool in it is empty. That is why a process rarely shrinks after freeing many objects. The details are in The allocator: heap, arenas, pools, blocks.

Where it breaks in production

Latency spikes from full collections

A large heap of long-lived containers (a cache of ORM objects, a big parsed document) makes every full collection walk all of it. The symptom is a p99 spike every few minutes with nothing in the request path to explain it; the gc.callbacks histogram above confirms it. Fewer long-lived containers, gc.freeze() for objects that never die, or raised thresholds are the fixes, measured before and after.

Copy-on-write lost in prefork servers

A gunicorn master imports the app, forks workers, and each worker’s first collection writes into the GC header of every inherited object, copying pages that would otherwise stay shared. The pattern: gc.disable() early in the master, load everything, gc.freeze() right before forking (frozen objects move to a permanent generation the collector ignores), and gc.enable() in each worker.

gc.disable() left on

Disabling the collector “for performance” is safe only where nothing creates cycles, and almost everything does: exceptions with tracebacks, closures, parent-child links, asyncio tasks. Memory grows without bound until the process is killed. Disable around a measured hot section, never globally.

Leaks the collector cannot see

A cycle that is still reachable is not garbage: a module-level cache, a stored exception holding its traceback, frames and locals, or an object kept alive by a registry. gc.get_referrers(obj) and tracemalloc snapshots diffed over time find what holds it (Memory profiling — tracemalloc, memray, and what holds memory).

The trade not taken

Reference counting with a backup cycle collector buys deterministic, immediate freeing for almost every object, at the cost of a write on every reference copy and a collector for the rest. A tracing collector alone, as in PyPy, the JVM or Go, avoids the per-reference writes and frees cycles naturally, but frees at unpredictable times.

So CPython code that relies on “freed when it goes out of scope” works on CPython and leaks resources on PyPy; code that uses with works everywhere.

Interview angle 12

  • “How does Python free memory?” Refcounting + cycle GC.
  • “What happens to a cycle if cycle GC is disabled?” It leaks: reference counting never reaches zero inside a cycle.
    • Follow-up: “How does the collector find garbage without a root set?” It subtracts the references that come from inside the tracked containers from their refcounts; whatever keeps a count above zero is referenced from outside, and anything not reachable from those is garbage.
    • Follow-up: “Your service shows a p99 spike every few minutes. Could it be the GC?” Time collections with gc.callbacks and correlate. Full collections over a large heap of long-lived containers pause the process for tens of milliseconds or more; gc.freeze() and fewer long-lived containers help.
  • “What are the default GC thresholds?” (2000, 10, 10) since 3.13, (700, 10, 10) before. And 3.14.0 to 3.14.4 ran an incremental two-generation collector that 3.14.5 reverted after memory-growth reports.
  • “Why call gc.freeze() before forking?” So the workers’ collections do not write into the GC headers of inherited objects, which would copy shared memory pages.
  • “Why does __del__ run immediately when an object goes out of scope?” Refcount → 0 — deterministic destruction.
  • “Why doesn’t every refcount drop trigger Python’s GC?” Refcount drop is the primary GC; the cycle collector is an additional layer on top.
  • “What does sys.getrefcount(x) return?” One more than the references you can see, because the call holds one - and the exact number is version-dependent: a local with one alias gives 3 on 3.12 and 2 on 3.14, which borrows references for locals. Never assert on it.
  • “Is __del__ guaranteed to run when the last reference goes?” On default CPython, yes, at once. Not on the free-threaded build for deferred-refcount objects (modules, top-level functions, class-body methods), which wait for a collection, and not on PyPy. Release resources with with, not by dropping references.
  • “What is an immortal object?” Since 3.12, an object such as None or a small int whose refcount is fixed and never written, so sharing it between threads costs nothing.
  • “How does free-threaded Python keep refcounts correct without the GIL?” Biased reference counting: a non-atomic count for the owning thread plus an atomic shared count, with deferred and per-thread counting for the most shared objects.
  • “What happens to an exception raised in __del__?” It cannot propagate, so it goes to sys.unraisablehook, which prints “Exception ignored while calling deallocator” (3.14) and carries on.
  • “Why doesn’t memory drop after I del a big list?” Freed blocks go back to pymalloc, and an arena returns to the OS only when all its pools are empty; see The allocator: heap, arenas, pools, blocks.