Python developers rarely think about memory allocation — and that's by design. But understanding what's happening behind the scenes helps you write more efficient code and debug tricky memory issues.
Reference Counting
Every object in Python carries a reference count: the number of places currently pointing to it. When you assign a variable, pass it to a function, or store it in a list, the count goes up. When references go out of scope or are deleted, it goes down.
import sys
a = [1, 2, 3]
print(sys.getrefcount(a)) # includes the temporary reference from getrefcount itself
b = a # reference count increases
del b # reference count decreases
When an object's reference count hits zero, CPython immediately deallocates it. This is why simple Python programs generally don't need manual memory management.
The Problem: Reference Cycles
Reference counting alone can't handle cycles — objects that reference each other but are otherwise unreachable:
class Node:
def __init__(self):
self.other = None
a = Node()
b = Node()
a.other = b
b.other = a
del a
del b
# a and b still reference each other — refcount never reaches zero
The Garbage Collector
To handle cycles, Python includes a cyclic garbage collector (in the gc module) that periodically scans for groups of objects that reference each other but are unreachable from anywhere else, and cleans them up.
import gc
gc.collect() # force a collection cycle
print(gc.get_stats()) # inspect collector statistics
This runs automatically in the background using a generational approach — like most garbage collectors, it assumes recently created objects are more likely to become garbage quickly, so it checks them more often than older, longer-lived objects.
Memory Pools: PyMalloc
For small objects, CPython uses its own memory allocator (pymalloc) that manages pools of fixed-size blocks, reducing the overhead of constantly asking the operating system for memory. This is why creating many small objects in Python is faster than you might expect.
Common Memory Pitfalls
Holding onto large objects unnecessarily:
def process():
huge_list = load_millions_of_records()
result = summarize(huge_list)
return result
# huge_list stays alive until the function returns
Circular references with __del__: objects with custom __del__ methods used to prevent cycle collection in older Python versions — modern CPython (3.4+) handles this safely.
Global caches that grow forever:
_cache = {}
def expensive_call(x):
if x not in _cache:
_cache[x] = compute(x)
return _cache[x]
# _cache never shrinks — this is a memory leak in long-running processes
Tools for Inspecting Memory
sys.getsizeof(obj)— size of a single object in bytestracemalloc— tracks where allocations happenobjgraph(third-party) — visualizes reference graphs to find leaks
import tracemalloc
tracemalloc.start()
# ... run code ...
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:5]:
print(stat)
Key Takeaway
Python's memory management combines reference counting for immediate cleanup with a cyclic garbage collector for the cases reference counting can't handle. Most of the time this is invisible — but in long-running services or memory-constrained environments, understanding it helps you spot leaks before they become production incidents.
Comments (0)
No comments yet
Be the first to share your thoughts!