Engineering Notes
Home/Languages/Python/Python's Memory Model and Garbage Collection
Languages · Python

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.

💻 Languages● Intermediate⏱ 3 min read🛠️ 2 exercises🎤 5 interview Q&As

🎯 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.

python
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 objects

Function 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. Use None and create inside.
  • [[0] * 3] * 3 creates 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.deepcopy copies nested objects.
  • Small integers (-5..256) and some strings are interned/cached: is may be true by accident; never use is to compare values (except None, 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.

python
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 generation

4Weak 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

  1. Mutable default arguments.
  2. Aliasing: modifying a list you received and surprising the caller.
  3. is for value comparison of numbers or strings.
  4. Unbounded caches and lru_cache on methods.
  5. Relying on __del__ for cleanup instead of context managers.

📋 Quick Reference

text
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; is checks 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

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • lru_cache on methods? It keeps self alive; 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.

1. def f(x, acc=[]) called twice without acc: the second call sees…
Defaults are evaluated once.
2. [[0]*3]*3 then grid[0][0]=1 changes…
Same row object 3 times.
3. What frees most objects in CPython?
Immediate.
4. What does WeakValueDictionary do?
Weak references.