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 halves, and interviewers want both. Half one is reference counting — the fast, deterministic mechanism that does almost all the work. Half two is the cyclic garbage collector, which exists for one reason: reference counting has a single, well-known blind spot.
Reference counting
Every Python object carries an ob_refcnt field. Binding a name, appending it to a list, passing it as an argument — each increments the count; letting one of those go decrements it. The instant the count hits zero, CPython runs the object’s deallocator — no GC pause, no delay, the memory comes back right away.
import sys
x = []
print(sys.getrefcount(x)) # 2: x itself + the temporary getrefcount argument
y = x
print(sys.getrefcount(x)) # 3
del y
print(sys.getrefcount(x)) # back to 2
sys.getrefcount always reads one higher than you’d expect, because passing the object into the function is itself a reference. Picture it as names pointing at one object:
name "x" ──► list object [ ob_refcnt = 2 ]
name "y" ──► (the same object)
del y → ob_refcnt drops to 1
del x → ob_refcnt drops to 0 → deallocator runs immediately
The blind spot: cycles
Reference counting can’t free a cycle:
a = []
b = [a]
a.append(b) # a points at b, b points at a
del a, b # both refcounts drop to 1, not 0 — leaked without a second mechanism
After del a, b, neither object is reachable from your code, yet each one’s count is stuck at 1 because they hold each other. On reference counting alone they’d leak forever. So CPython adds a generational mark-and-sweep collector that scans container objects (lists, dicts, instances), finds these unreachable islands, and frees them. Objects that survive a sweep are promoted to older generations and scanned less often — most objects die young, which makes that strategy cheap.
import gc
gc.collect() # force a full collection
print(gc.get_count()) # (young, middle, old) tallies
What you can actually control
gc.disable()— worth it in a long-running server if you can prove there are no cycles; it removes GC pauses entirely.__slots__— drops the per-instance__dict__, saving dozens of bytes per object at scale.weakref— a reference that doesn’t bumpob_refcnt, which is how you point at something without keeping it alive (and how you break cycles on purpose):
import weakref
class Node:
def __init__(self, value):
self.value = value
n = Node(42)
ref = weakref.ref(n)
print(ref()) # the Node — still alive
del n
print(ref()) # None — the last real reference dropped, so the object is gone