01 — Object References, Mutability, Recycling & Memory Leaks

At a glance

After completing this lesson you can:

Time: ~50 minutes · Prerequisites: 02 — History Hygiene

Reading Companion

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

3. CPython Memory Management & Garbage Collection

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

  1. 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.
  2. Rung 2 (append_tag_safely — Fix the AI Bug): Fix the mutable default argument trap in a function taking tags: list[str] | None = None.
  3. 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).
  4. Rung 4 (refcount_delta_when_stored): Prove that storing an object in a container adds exactly one reference: measure sys.getrefcount() before and after building holder = [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.
  5. Rung 5 (BoundedLRUCache): Build an in-memory cache with bounded size max_size using collections.OrderedDict, evicting the least-recently-used item on overflow to prevent memory leaks.

Wrapping up

Can you explain?

  1. Why is x is None preferred over x == None?
  2. Why does batch: list = [] leak data across different HTTP requests?
  3. How does Python's cyclic garbage collector detect reference cycles that reference counting misses?

Further Reading & References

Show reference solution notes

Solution & Engineering Notes — 01 Object References & Memory

  1. is vs ==: == checks value equality (__eq__), calling Python methods that can be slow or overridden. is checks exact memory address identity (id(a) == id(b)), which is a single assembly pointer comparison. Singletons like None must always be checked with is.
  2. 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.

Confidence:

Completion edits PROGRESS.md; commit it with the lesson.