How does Python memory management work?
Python memory management (in CPython) is automatic — you don’t call malloc/free. It combines reference counting, a cyclic garbage collector, and an internal private heap with specialized allocators.
Reference counting — the primary mechanism: every Python object carries a count of how many references point to it. When the count drops to zero, the object’s memory is reclaimed immediately and its __del__ (if any) runs.
import sys
a = []
sys.getrefcount(a) # includes the temporary passed to getrefcount()
b = a
# two names refer to the same list — refcount increasedReference counting is deterministic and low-latency — memory is freed as soon as it’s unreachable — but it can’t handle reference cycles (e.g. a.b = b; b.a = a).
Cyclic garbage collector — handles cycles: the gc module runs a generational GC that periodically scans container objects to find unreachable cycles and break them. It uses three generations (0, 1, 2) with increasing scan thresholds; short-lived objects are collected quickly in gen 0, surviving ones age into older generations.
import gc
gc.collect() # force a collection
gc.get_threshold() # per-generation thresholds
gc.disable() # rare — e.g. during allocation-heavy hot sectionsThe private heap and allocator stack: Python objects live on a heap managed by the interpreter, not directly by malloc:
Object-specific allocators — ints, floats, tuples, dicts have free lists for fast reuse.
PyMalloc /
pymalloc— a small-block allocator (≤ 512 bytes) that carves up 4 KB pools into fixed-size blocks for speed and low fragmentation.The system allocator — larger requests fall through to the C runtime’s
malloc.
Interning and caching: CPython caches small integers (-5..256), interns many short strings, and keeps singleton objects (None, True, False) — which is why is sometimes appears to work on values you shouldn’t compare that way.
Weak references (weakref) let you hold a reference without incrementing the refcount — useful for caches and observer patterns that shouldn’t keep objects alive.
__slots__ for compact objects: defining __slots__ on a class replaces the per-instance __dict__ with a fixed-size struct, saving memory and attribute-lookup time for classes created in the millions.
Common sources of memory growth:
Unbounded caches (dicts that never evict) — use
functools.lru_cache(maxsize=...)or size-limited caches.Closures capturing large objects — check what your lambdas and partials actually reference.
Circular references holding resources — break cycles manually or keep
__del__-free and let the cyclic GC collect.Keeping references to big intermediates across a loop — reassign or
delto drop them.Module-level state, global registries, and long-lived caches.
Tools: tracemalloc (built-in, tracks allocations by source line), sys.getsizeof, gc.get_objects, and third-party profilers like memory_profiler and objgraph.
Interview-ready summary: CPython uses reference counting for prompt cleanup plus a generational cyclic GC for reference cycles, on top of a private heap with specialized small-object allocators and free lists. Reference counts are incremented and decremented automatically by the interpreter; the gc, weakref, and tracemalloc modules give you the controls and visibility when you need them.
import sys
import gc
# Reference counting
a = [1, 2, 3]
print(sys.getrefcount(a)) # 2 (a + the getrefcount argument)
b = a
print(sys.getrefcount(a)) # 3 (a, b, + argument)
del b
print(sys.getrefcount(a)) # 2
# Circular reference (reference counting alone can't handle)
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b # a -> b
b.ref = a # b -> a (cycle!)
del a
del b
# Refcounts are still > 0 (circular reference)
# gc.collect() will find and free them
# __slots__ for memory optimization
class UserWithSlots:
__slots__ = ['name', 'email'] # No __dict__
def __init__(self, name, email):
self.name = name
self.email = email
# Uses ~40% less memory than regular class
# Cannot add arbitrary attributes
# Garbage collector info
print(gc.get_threshold()) # (700, 10, 10) — collection thresholds
gc.collect() # Force collectionsys.getrefcount shows how many references exist (always +1 for the function argument itself). The Node cycle creates a circular reference that reference counting cannot free — the gc module detects and collects these. slots eliminates the per-instance dict, significantly reducing memory for classes with many instances. gc.get_threshold shows the generational collection thresholds.
Know both mechanisms: reference counting (immediate, deterministic) and cyclic GC (handles reference cycles). The circular reference example is the classic interview scenario. slots for memory optimization shows practical knowledge. sys.getrefcount and gc.collect are useful debugging tools.