Memory Management in Python: Reference Counting and the Garbage Collector
- Every Python object carries a count of references pointing to it; CPython frees the object the moment that count reaches zero.
- Reference counting alone can't reclaim two objects that reference each other in a cycle, since neither count ever hits zero on its own.
- The gc module runs a separate generational collector that specifically finds and breaks reference cycles no longer reachable from your code.
- Objects are grouped into three generations by age; young, short-lived objects are scanned far more often than old, long-lived ones.
Reference counting: freed the instant nobody points at it
Every object in CPython has a hidden integer field, its reference count, tracked in the object's C struct. Binding a name to an object, appending it to a list, or passing it as a function argument all increment that count; a name going out of scope, a del statement, or reassigning a variable decrement it. The moment the count reaches zero, CPython frees the object immediately — there's no delay, no separate sweep pass to wait for, which is different from garbage collectors in languages like Java or Go.
import sys
a = object()
print(sys.getrefcount(a)) # 2: the name 'a' plus the temporary argument to getrefcount
b = a
print(sys.getrefcount(a)) # 3: now 'a' and 'b' both reference it
del b
print(sys.getrefcount(a)) # back to 2
This is why closing a file handle or database connection often "just works" when the object goes out of scope in CPython — the object's __del__ runs as soon as the last reference disappears. Relying on that timing is risky though: it's a CPython implementation detail, not a language guarantee, and other implementations like PyPy don't free objects the instant their count hits zero.
Watching reference counts with sys.getrefcount
sys.getrefcount() is useful for understanding what's keeping an object alive, but it always reports one more than you'd expect, because passing the object into getrefcount() itself creates a temporary reference for the duration of the call. It's a diagnostic tool, not something to build application logic around.
The problem cycles cause for pure reference counting
Reference counting has one structural blind spot: two or more objects that reference each other, directly or through a chain, keep each other's count above zero forever, even after nothing outside the cycle can reach them:
class Node:
def __init__(self, name):
self.name = name
self.parent = None
self.children = []
def add_child(self, child):
child.parent = self # child now references parent
self.children.append(child) # parent now references child
root = Node("root")
leaf = Node("leaf")
root.add_child(leaf)
del root
del leaf
# Neither object's refcount reaches zero from these two del statements alone:
# root.children still holds a reference to leaf, and leaf.parent still holds
# a reference to root. Pure refcounting would leak this pair forever.
Parent/child trees, doubly linked lists, and objects that register callbacks on each other are the most common places this shows up. Without something extra, that memory would simply never come back, even though the program can no longer reach either object through any live variable.
The generational cycle collector in gc
CPython solves this with a second, separate mechanism: the gc module's cyclic garbage collector, which runs periodically alongside reference counting rather than instead of it. It specifically looks for groups of objects that reference each other but are unreachable from anything outside the group, and frees the whole group at once.
To make this efficient, objects are organised into three generations by how long they've survived. New objects start in generation 0. Every time gen-0 fills past a threshold, it's scanned; survivors are promoted to generation 1, and objects that survive enough gen-1 scans are promoted to generation 2. Generation 0 is scanned far more often than generation 2, on the empirically reasonable assumption that most garbage is short-lived and old objects are less likely to have just become unreachable.
import gc
gc.collect() # force a full collection right now
print(gc.get_count()) # (gen0 objects, gen1 objects, gen2 objects) since last collection
print(gc.get_threshold()) # (700, 10, 10) by default: gen0 runs after ~700 allocations
# Track down what's actually keeping something alive
import sys
target = SomeObject()
referrers = gc.get_referrers(target)
Objects that define a custom __del__ method used to be a special case — before Python 3.4, cycles containing such objects couldn't be collected at all and were reported in gc.garbage. PEP 442 fixed that; __del__ objects in cycles are collected normally now, though relying on finalizer ordering across a cycle is still fragile and best avoided.
What this means for your code
For the overwhelming majority of Python code, none of this needs active management — reference counting plus the cycle collector handle memory transparently. Two situations are worth knowing about specifically. First, if you're building structures with parent/child back-references and you're seeing memory grow unexpectedly, the weakref module lets one side of the relationship hold a non-counting reference, so the cycle never forms in the first place. Second, if you're debugging what looks like a leak, gc.set_debug(gc.DEBUG_LEAK) and gc.get_objects() will show you what the collector currently considers alive, which is usually enough to spot an unexpected reference chain. The official gc module documentation covers the debug flags and thresholds in full.