Why this matters
You never call free() in Python, yet memory is reclaimed. Knowing how explains real behavior: why a file closes as soon as its last reference disappears, why a cache can leak memory forever, why del sometimes appears to do nothing, and why a program that builds object graphs (trees with parent pointers, observers, caches) may grow until the cycle collector runs. It also clears up folklore about is, small integers, and sys.getsizeof.
Objects and references, recap
Every value in CPython is an object in heap memory, and every name, list slot, attribute and dict value holds a reference (a pointer) to an object, never the object itself:
names heap
a ----------> +-----------------+
| list |
b ----------> | refcount: 2 |
| items: [*, *, *]|---> 1, 2, 3
+-----------------+
Each object header contains a reference count: how many references point at it right now. CPython adjusts it constantly. Binding a name, putting an object into a list, passing it as an argument: increment. Rebinding the name, removing it from the list, the function returning and its local variables going away: decrement.
Reference counting
When the count drops to zero, nothing can reach the object any more, so CPython frees it immediately: it runs the object's cleanup (for a file, that closes it; for a class with __del__, that method is called), decrements the counts of everything it referred to (which may cascade), and returns the memory to its allocator.
You can peek at the count:
import sys
x = []
print(sys.getrefcount(x)) # 2
y = x
print(sys.getrefcount(x)) # 3
del y
print(sys.getrefcount(x)) # 2
Why 2, not 1? Passing x to getrefcount creates a temporary reference for the duration of the call. Treat the absolute number as a curiosity (it differs between versions and for objects such as small integers, which are "immortal" since 3.12); the changes are what is instructive.
del removes a reference, not an object.
class Resource:
def __init__(self, name):
self.name = name
def __del__(self):
print("closing", self.name)
r = Resource("a")
keep = [r]
del r
print("after del r")
keep.clear()
print("after clear")
after del r
closing a
after clear
The object survives del r because the list still refers to it, and dies the instant the list drops its reference. This deterministic, immediate cleanup is a CPython implementation detail; other implementations (PyPy) free objects later. Portable code closes files with with, not by hoping the last reference vanishes on time.
Strengths and costs. Reference counting frees memory promptly and predictably, with no long pauses. The price is that every reference operation touches a counter (one reason the GIL exists, as the next lesson shows) and that it cannot handle cycles.
Cycles
class Node:
pass
a = Node()
b = Node()
a.other = b
b.other = a
del a, b
+--------+ other +--------+
| Node 1 | ------> | Node 2 |
| rc: 1 | <------ | rc: 1 |
+--------+ other +--------+
(no names point here any more)
Each node's count is 1, held by the other, so neither ever reaches zero. They are garbage (unreachable from the program) but reference counting will never free them. Cycles are common: a tree whose children point to their parent, a doubly linked list, an object that stores a bound method of itself, an exception holding a traceback that references the frame that holds the exception.
The cycle collector: gc
CPython adds a cyclic garbage collector, the gc module. It tracks only container objects (those that can refer to others: lists, dicts, instances, and so on) and periodically does the following for a group of them:
- Copy each object's refcount into a scratch counter.
- For every reference between objects in the group, subtract one from the target's scratch count.
- Objects whose scratch count is still above zero are referenced from outside the group, so they are reachable; so is everything they reach.
- Everything else is unreachable cycles: free it.
Collection is triggered by allocation counts. Objects are sorted into generations: new objects are checked often; objects that survive a collection are promoted and checked less often, based on the observation that most objects die young. You can control it:
import gc
gc.collect() # run a full collection now, returns number of unreachable objects found
gc.disable() # stop automatic collection (reference counting still works)
gc.enable()
gc.get_count() # allocation counters per generation
Some programs disable the collector or raise thresholds for speed, which is safe only if they create no cycles worth reclaiming.
Weak references
Sometimes you want to refer to an object without keeping it alive, for example a cache of objects that should disappear when nobody else uses them, or a child that points back to its parent. That is a weak reference:
import weakref
class Big:
pass
obj = Big()
ref = weakref.ref(obj)
print(ref() is obj) # True: calling the ref returns the object
del obj
print(ref()) # None: the object was freed
A weakref does not count toward the reference count. weakref.WeakValueDictionary and weakref.WeakSet build caches and registries on this idea, and weakref.finalize(obj, func) registers a cleanup callback more reliably than __del__.
Sizes and folklore
sys.getsizeof returns the size in bytes of one object, shallowly:
import sys
print(sys.getsizeof([]) < sys.getsizeof([1, 2, 3])) # True
A list's size covers its header and pointer array, not the objects in it; a dict's covers its tables, not the keys and values. Exact numbers vary by version and platform, so never hard-code them. To find where memory goes in a real program, use tracemalloc, which records allocations by source line.
Small integers and interning. CPython pre-creates the integers -5 to 256 and reuses them, and it interns many strings (identifiers, short literal strings), keeping one shared copy. So:
a = 256
b = 256
print(a is b) # True: same cached object
but the same test with larger numbers or computed strings may print False, and may differ between the REPL and a script, because constants in one compiled unit can be shared. This is an optimization you must never rely on: compare values with ==, and use is only for identity checks like x is None.
Memory goes back slowly. CPython's small-object allocator (pymalloc) grabs memory from the operating system in large arenas and reuses freed blocks internally. After you delete a huge list of small objects, the process size seen by top may not shrink much; the memory is reused by Python, not necessarily returned to the OS.
Common mistakes
- Relying on
__del__for important cleanup. It runs at an implementation-dependent time (later on PyPy, possibly never at interpreter exit). Usewithand context managers. - Unbounded caches. A module-level dict that stores every result keeps every object alive forever. Bound it (
functools.lru_cache(maxsize=...)) or use weak references. - Keeping a reference by accident.
alive = ref()stores a strong reference; the object will not die whilealiveexists. - Using
isto compare numbers or strings. Python 3.8+ warns:SyntaxWarning: "is" with 'int' literal. Did you mean "=="? - Trusting
getsizeoffor total size. It is shallow.
Recap
- Every object has a reference count; CPython frees an object the moment its count hits zero, running its cleanup immediately.
del nameremoves one reference, not the object.- Reference cycles never reach zero; the generational cycle collector (
gc) finds and frees unreachable cycles periodically. - Weak references (
weakref.ref,WeakValueDictionary) refer to objects without keeping them alive. sys.getsizeofis shallow; small integers and some strings are shared, so never useisfor value comparison.
Going further: read the "Design of CPython's Garbage Collector" document in the CPython developer guide, and try tracemalloc.start() followed by tracemalloc.take_snapshot().statistics("lineno")[:5] on a program of your own.