Python's Memory Model and Garbage Collection
How Python manages memory: names bound to objects, mutability and aliasing traps, reference counting plus the cycle collector, weak references, __slots__, and how to find memory growth.
🎯 What you'll learn
- Names, objects, identity and aliasing
- Mutability gotchas and copying
- Reference counting and the generational cycle collector
- Weak references, __slots__ and memory profiling
📚 Before you start
1Names, Objects and References
A Python variable is a name bound to an object. Assignment never copies; it binds another name to the same object. Every object has an identity (id()), a type and a value; is compares identity, == compares value.
a = [1, 2]
b = a # same list
b.append(3) # a is now [1, 2, 3]
c = a[:] # shallow copy: a new list holding the same element objectsFunction arguments are passed by object reference: mutating a mutable argument is visible to the caller; rebinding the parameter name isn't.
2Mutability Gotchas
- Mutable default arguments are created once at function definition:
def add(x, items=[])shares one list across calls. UseNoneand create inside. [[0] * 3] * 3creates three references to the same inner list; use[[0] * 3 for _ in range(3)].- Shallow vs deep copy:
copy.copy/ slicing copy the container only;copy.deepcopycopies nested objects. - Small integers (-5..256) and some strings are interned/cached:
ismay be true by accident; never useisto compare values (exceptNone,True,False).
3Reference Counting and the Cycle Collector
CPython frees most objects immediately when their reference count drops to zero (deterministic: files closed by refcount are closed right away, though you should still use with). Reference cycles (a.b = b; b.a = a) can't reach zero, so a generational cycle collector (gc module, three generations) periodically finds and frees unreachable cycles.
import sys, gc
sys.getrefcount(obj) # +1 for the temporary argument reference
gc.collect() # force a cycle collection (debugging/tests)
gc.get_count() # allocations since last collection per generation4Weak References
weakref.ref(obj) and weakref.WeakValueDictionary reference objects without keeping them alive: when the last strong reference disappears, the weak reference returns None and the dictionary entry vanishes. Useful for caches of large objects and observer registries that shouldn't leak.
5Memory Efficiency
__slots__removes the per-instance__dict__, saving memory for many small objects (and preventing new attributes).- Generators and iterators process streams without materializing lists.
array,bytes, NumPy arrays store primitives compactly; a list of ints stores pointers to int objects (~28 bytes each).- Measure with
tracemalloc(allocations by line),sys.getsizeof(shallow size), and memory profilers (memray, scalene).
6Leaks in Python
Memory "leaks" are objects kept reachable: global caches that grow (unbounded lru_cache(maxsize=None) on methods also keeps self alive), lists of callbacks, reference cycles with __del__ in old versions, C extensions. tracemalloc snapshots compared over time show where memory grows.
⚠️ Common Pitfalls
- Mutable default arguments.
- Aliasing: modifying a list you received and surprising the caller.
isfor value comparison of numbers or strings.- Unbounded caches and
lru_cacheon methods. - Relying on
__del__for cleanup instead of context managers.
📋 Quick Reference
names bind to objects · assignment never copies · is = identity, == = value
mutable defaults → None · shallow (copy, [:]) vs deepcopy · [[0]*n]*m shares rows
refcounting frees immediately · gc collects cycles (3 generations) · weakref for non-owning refs
__slots__ for many small objects · tracemalloc / memray to find growth · with-statements for cleanup🧠 Key takeaways
- Assignment binds names, it never copies;
ischecks identity. - Beware mutable defaults, shared rows and shallow copies.
- Refcounting frees objects immediately; the gc module collects cycles.
- Use weakref for non-owning caches and __slots__ for many small objects.
🎤 Interview Q&A
Q1How does Python manage memory?core▾
CPython uses reference counting: each object counts references and is freed when the count hits zero. A generational cycle collector (gc module) finds groups of objects that reference each other but are unreachable. Memory comes from pymalloc arenas for small objects.
- Can memory return to the OS? Not always; arenas are released only when fully empty.
Q2Explain shallow vs deep copy.core▾
A shallow copy creates a new container holding references to the same elements (list[:], copy.copy); a deep copy recursively copies nested objects (copy.deepcopy). Mutating nested objects in a shallow copy affects the original.
- When is deepcopy slow? Large nested structures; consider immutable data instead.
Q3What is the mutable default argument problem?core▾
Default values are evaluated once at function definition, so a mutable default (list/dict) is shared between calls. Use None and create the object inside the function.
- Dataclass equivalent?
field(default_factory=list).
Q4When would you use weak references?senior▾
For caches or registries that shouldn't keep objects alive (large images, observers), via weakref.ref, WeakValueDictionary, WeakSet, or weakref.finalize for cleanup callbacks.
- Can you weakref every object? Not ints/tuples; classes with __slots__ need '__weakref__' slot.
Q5How do you find a memory leak in a Python service?core▾
Watch RSS over time, take tracemalloc snapshots and compare top allocation sites, inspect object counts with gc.get_objects/objgraph, and look for growing caches, global lists, or unbounded lru_cache. memray and scalene help profile.
- lru_cache on methods? It keeps
selfalive; use maxsize or cache per instance.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.
✅ Check yourself
Answer the questions — feedback is instant.
def f(x, acc=[]) called twice without acc: the second call sees…[[0]*3]*3 then grid[0][0]=1 changes…