How does Python manage memory — reference counting and the garbage collector?
CPython uses reference counting as its primary mechanism: every object carries a counter of how many names or containers point to it, and the object is freed immediately when the count hits zero. Because reference counting cannot detect cycles, Python also runs a cyclic garbage collector that periodically finds and collects groups of objects that refer only to each other.
How to think about it
This question has two parts and interviewers want both. Part one: reference counting, which is the fast deterministic mechanism. Part two: the cyclic GC, which exists specifically because reference counting has one well-known blind spot.
Reference counting
Every Python object has an ob_refcnt field. Binding a name, appending to a list, or passing an argument increments it; the reverse decrements it. When it reaches zero, CPython calls the object’s deallocator immediately — no GC pause, no delay.
import sys
x = []
print(sys.getrefcount(x)) # 2: x + the getrefcount argument itself
y = x
print(sys.getrefcount(x)) # 3
del y
print(sys.getrefcount(x)) # back to 2
sys.getrefcount always reports one more than you expect because passing the object to the function is itself a reference.
How memory flows: a diagram
name "x" ──► list object [ ob_refcnt = 2 ]
name "y" ──► (same object)
del y → ob_refcnt drops to 1
del x → ob_refcnt drops to 0 → deallocator runs immediately
The cyclic GC
Reference counting fails on cycles:
a = []
b = [a]
a.append(b) # a refs b, b refs a
del a, b # both refcounts drop to 1, not 0 — leak without the GC
Even after del a, b, neither object can be reached from your program, but their refcounts are stuck at 1 because they point at each other. Without a second mechanism they would leak forever.
CPython’s gc module runs a tri-generational mark-and-sweep over container objects (lists, dicts, instances). Objects that survive a collection get promoted to older generations, which are collected less often — most objects die young, so this is efficient.
import gc
gc.collect() # force a full collection
print(gc.get_count()) # (young, middle, old) object counts
gc.disable() # safe only if you guarantee no cycles
What you can control
gc.disable()— worthwhile in long-running servers if you can prove no cycles; removes GC pauses.__slots__on classes — eliminates the per-instance__dict__, saving dozens of bytes per object at scale.weakref— a reference that does not incrementob_refcnt, breaking cycles intentionally.
import weakref
class Node:
def __init__(self, value):
self.value = value
n = Node(42)
ref = weakref.ref(n)
print(ref()) # Node object
del n
print(ref()) # None — object is gone, refcount hit zero immediately