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.
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.13The 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
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 itemsWhen 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:
a = []
b = []
a.append(b)
b.append(a)
del a, b # each still holds the other: refcounts stay at 1That 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.
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: garbageWhat 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.
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 refcountingGenerations 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
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
import gc
x = SomeObject()
gc.get_referrers(x) # list of objects that reference x
gc.get_referents(x) # list of objects x referencesUseful 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.
Related
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.callbacksand 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 withwith, not by dropping references. - “What is an immortal object?” Since 3.12, an object such as
Noneor 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 tosys.unraisablehook, which prints “Exception ignored while calling deallocator” (3.14) and carries on. - “Why doesn’t memory drop after I
dela 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.