Skip to content

Memory Management

CPython manages memory primarily through reference counting: every object tracks how many references point to it, and is deallocated the instant that count hits zero. A secondary generational garbage collector exists specifically to catch reference cycles (objects referencing each other in a loop), which reference counting alone can never clean up.

Thereโ€™s no โ€œbeforeโ€ here โ€” this is how CPython has always worked, so the useful comparison is against languages with a purely tracing garbage collector (Java, Go, C#): those never free an object until a GC pass runs and discovers itโ€™s unreachable, even if nothing references it the instant it becomes unreachable. Pythonโ€™s reference counting frees most objects immediately and deterministically.

import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2 -- 'a' itself, plus the temporary arg to getrefcount()
b = a # new reference to the same list
print(sys.getrefcount(a)) # 3
del b # one less reference
print(sys.getrefcount(a)) # back to 2
del a # refcount hits 0 -- the list is deallocated immediately
# Reference cycles: refcounting alone can't free these
import gc
class Node:
def __init__(self):
self.other = None
a = Node()
b = Node()
a.other = b
b.other = a # a and b now reference each other
del a
del b
# Neither object's refcount ever reaches 0 -- they still reference each
# other. The generational garbage collector (gc module) finds and frees
# cycles like this periodically, even though refcounting alone can't.
gc.collect() # can be triggered manually; also runs automatically
  • Deterministic, immediate cleanup for the common case: most objects (no cycles involved) are freed the instant their last reference disappears โ€” no unpredictable GC pause, and with-block resources close/release memory predictably.
  • Cycle detection covers the gap: the generational GC specifically targets reference cycles, so you get the benefits of refcountingโ€™s immediacy and correctness for cyclic structures, without manually breaking every cycle yourself.
  • __del__ and context managers become reliable: because refcounting is deterministic, __del__ runs (for non-cyclic objects) predictably when the last reference goes away, and with blocks can rely on cleanup happening right at the end of the block.
  • The generational GC only needs to run on objects that could participate in a cycle โ€” CPython tracks โ€œcontainerโ€ objects (that can hold references to other objects, like lists/dicts/instances) separately from simple objects (ints, strings) that structurally canโ€™t form cycles and are handled by refcounting alone.
  • weakref (see the dedicated page) lets you reference an object without increasing its refcount โ€” useful for caches or observer patterns where you donโ€™t want your reference to be the thing keeping an object alive.
  • sys.getrefcount(x) is always at least 1 higher than youโ€™d expect, because passing x as an argument to getrefcount itself creates a temporary reference.
  • CPythonโ€™s refcounting is why the GIL exists in the first place โ€” incrementing/decrementing a refcount from multiple threads at once without a lock would corrupt the count.
  1. What happens the instant an objectโ€™s reference count reaches zero?

    AnswerIt's deallocated immediately โ€” CPython doesn't wait for a garbage-collection pass for non-cyclic objects.
  2. Why canโ€™t reference counting alone free two objects that reference each other?

    AnswerEach one still holds a reference to the other, so neither's count ever reaches zero, even after nothing external references either of them โ€” that's exactly what a reference cycle is.
  3. What Python mechanism specifically exists to clean up reference cycles?

    AnswerThe generational garbage collector (the `gc` module) โ€” it periodically scans for and frees groups of objects that are unreachable from outside but still reference each other.