01 — Object References, Mutability, Recycling & Memory Leaks
At a glance
After completing this lesson you can:
- Trace CPython object allocation, identity (
id()), and equality (==vsis). - Explain CPython's dual garbage collection strategy: reference counting (
sys.getrefcount) and the generational cyclic collector (gc). - Diagnose and eliminate memory leaks caused by circular references with
__del__, module-level mutable caches, and closures.
Time: ~50 minutes · Prerequisites: 02 — History Hygiene
Reading Companion
- Book: Fluent Python (2nd Edition) — Luciano Ramalho
- Chapter 6: Object References, Mutability, and Recycling (Variables as labels, shallow vs deep copy, mutable defaults, weak references).
Why this matters in production
Long-running Python services (FastAPI workers, Celery daemons) often suffer from "creeping memory leaks": RAM usage slowly climbs until Linux OOM-kills the process at 04:00 AM. In almost every case, the cause is not C-extension memory corruption, but mutable objects trapped in global state, circular references holding unclosed resources, or function default arguments mutating across requests.
Deep dive
1. Variables are Labels, Not Boxes
In Python, variables are names attached to objects in heap memory.
a = [1, 2, 3]
b = a # b references the EXACT same object in memory!
b.append(4)
print(a) # [1, 2, 3, 4]
2. Shallow vs Deep Copy
- Shallow Copy (
copy.copy(x)orlist(x)): Duplicates the outer container, but child items inside point to the exact same references. - Deep Copy (
copy.deepcopy(x)): Recursively duplicates the container and every nested mutable object inside.
3. CPython Memory Management & Garbage Collection
- Primary: Reference Counting. Every object has an
ob_refcnt. When it hits0, memory is freed immediately. - Secondary: Generational Cyclic GC. Detects isolated islands of objects pointing to each other (
A -> B -> A).
Hands on
python3 phase0-foundations/03-python-internals/01-references-mutability-and-memory/examples/01_demo.py
python3 phase0-foundations/03-python-internals/01-references-mutability-and-memory/examples/02_trap.py
AI Trap & Code Review
# BAD AI-GENERATED CODE — THE MUTABLE DEFAULT ARGUMENT TRAP
def add_event_to_batch(event: dict, batch: list = []) -> list:
batch.append(event)
return batch
In Python, default arguments are evaluated once at function definition time, NOT per call! Every request that omits batch shares and mutates the exact same list in memory.
Exercises
- Rung 1 (
safe_clone_event): Make a deep copy of an event payload dictionary containing nested lists and dicts, ensuring mutations to the clone do not affect the original. - Rung 2 (
append_tag_safely— Fix the AI Bug): Fix the mutable default argument trap in a function takingtags: list[str] | None = None. - Rung 3 (
detect_object_sharing): Given two nested dictionaries, return the list of dot-notated key paths where values share the exact same object identity (is). - Rung 4 (
refcount_delta_when_stored): Prove that storing an object in a container adds exactly one reference: measuresys.getrefcount()before and after buildingholder = [target]and return the difference. Measure a delta, never an absolute number: absolute refcounts differ across CPython versions (immortal objects in 3.12). Use a container rather than a plain local alias, because CPython 3.14 may borrow a reference for a simple local load instead of incrementing the count. - Rung 5 (
BoundedLRUCache): Build an in-memory cache with bounded sizemax_sizeusingcollections.OrderedDict, evicting the least-recently-used item on overflow to prevent memory leaks.
Wrapping up
Can you explain?
- Why is
x is Nonepreferred overx == None? - Why does
batch: list = []leak data across different HTTP requests? - How does Python's cyclic garbage collector detect reference cycles that reference counting misses?
Further Reading & References
- CPython Internals & Data Model Reference
- PEP 544 — Protocols: Structural subtyping (static duck typing)
- PEP 557 — Data Classes
- Python Garbage Collector (gc module) Architecture
Show reference solution notes
Solution & Engineering Notes — 01 Object References & Memory
isvs==:==checks value equality (__eq__), calling Python methods that can be slow or overridden.ischecks exact memory address identity (id(a) == id(b)), which is a single assembly pointer comparison. Singletons likeNonemust always be checked withis.- Mutable defaults: Python binds default arguments to the function object at definition time (
func.__defaults__). Mutating that list mutates the function object's internal attribute permanently.
Self-check
Run python3 -m unittest phase0-foundations/03-python-internals/01-references-mutability-and-memory/test_exercises.py -v until green, then record it here. Saved to progress/rungs.json; commit it.
Completion edits PROGRESS.md; commit it with the lesson.