Diagnosing leaks, slow finalizers and memory growth in long-running services needs this model.
CPython's primary memory management is reference counting. Every object stores how many references point to it; binding a name, putting an object in a container or passing it as an argument increments the count, and del, rebinding or leaving scope decrements it. When the count hits zero the object is freed immediately — that's why a file opened without with in CPython usually closes promptly (other implementations like PyPy don't guarantee this, so never rely on it). sys.getrefcount(x) shows the count, plus one for its own argument.
Reference counting can't reclaim cycles: a parent pointing to a child that points back keeps both counts above zero forever. The cyclic garbage collector in the gc module handles that. It tracks container objects and periodically finds groups only reachable from each other. In 3.13 it is generational: new objects are scanned often, survivors are promoted to older, rarely scanned generations (gc.get_threshold() shows the triggers). gc.collect() forces a full collection, and gc.freeze() moves everything into a permanent generation, a trick pre-fork servers use to avoid copy-on-write.
Weak references (weakref.ref, WeakValueDictionary, WeakKeyDictionary) point to an object without keeping it alive — the right tool for caches and back-pointers that would otherwise form cycles. weakref.finalize is a safer cleanup hook than __del__, which runs at unpredictable times and can't rely on globals during shutdown.
Some objects are immortal since 3.12 (PEP 683): None, True, small ints and many interned strings have a fixed, huge refcount that's never modified, reducing cache traffic. Freed memory goes back to CPython's small-object allocator (pymalloc), so process RSS may not shrink. Use tracemalloc to find which lines allocate the memory that grows.
Example
import gc
import sys
import weakref
class Node:
def __init__(self, name):
self.name, self.peer = name, None
a = Node("a")
print(sys.getrefcount(a)) # 2: the name `a` + the call's argument
x, y = Node("x"), Node("y")
x.peer, y.peer = y, x # reference cycle
probe = weakref.ref(x) # doesn't keep x alive
del x, y
print(probe() is not None) # True: refcounting alone can't free a cycle
print(gc.collect() >= 2) # True: the cycle collector found them
print(probe()) # None
cache = weakref.WeakValueDictionary()
cache["a"] = a
del a
print("a" in cache) # False: freed as soon as the last ref went
print(sys.getrefcount(None) > 10**8) # True: None is immortal (3.12+)Remember
- CPython frees objects when their refcount drops to zero
- The cyclic GC reclaims reference cycles that refcounting can't
- Use weakrefs for caches and back-pointers; prefer
weakref.finalizeto__del__
Watch out for
- Relying on
__del__for important cleanup such as closing connections.