Skip to content
Hello Python
5/7

Memory Model & Garbage Collection

Topic 5 of 7, with 3 concept checks. Reference counting, cycles, and __slots__

Reason about references, lifetimes, and collection

Object lifetime

Count references conceptually, identify cycles and weak references, and distinguish object identity from storage details before drawing conclusions about Python memory behavior.

Core lesson 01

CPython uses reference counting as its primary strategy: every object tracks how many references point to it, and is freed immediately when that count hits zero.

Every Python object has a hidden reference count. Assigning it to a variable, appending it to a list, or passing it to a function all increment the count; deleting a reference, reassigning a variable, or a scope ending decrements it. The moment the count reaches zero, CPython frees the object immediately and deterministically — simple objects are cleaned up right away rather than accumulating until some later sweep.

Python example
import sys

a = [1, 2, 3]
print(sys.getrefcount(a))   # 2 (the variable `a`, plus the temporary arg to getrefcount)
b = a
print(sys.getrefcount(a))   # 3 -- another reference
del b
print(sys.getrefcount(a))   # back to 2

What to remember

How does CPython primarily manage memory — and when does it actually free an object?

Common footguns

  • Assuming Python has no memory management at all — reference counting IS a form of it, just automatic; the cyclic garbage collector (next question) only handles cases reference counting can't.

Core lesson 02

Reference counting can't free reference cycles since their counts never naturally reach zero. Python's separate cyclic garbage collector periodically scans for and cleans up exactly these unreachable cycles.

If object A holds a reference to B, and B holds a reference back to A, and nothing else references either, both still have a reference count of 1 (from each other) — reference counting alone would leak them forever. Python's generational cyclic garbage collector (the gc module) periodically looks for groups of objects unreachable from the rest of the program even though their internal reference counts are nonzero, and collects them.

Python example
import gc

class Node:
    def __init__(self):
        self.other = None

a, b = Node(), Node()
a.other = b
b.other = a       # a cycle: a -> b -> a
del a, b          # no external references left, but the cycle still exists internally
gc.collect()      # the cyclic collector finds and frees the cycle

What to remember

If reference counting frees objects immediately, why does Python still need a separate garbage collector?

Common footguns

  • Relying on __del__ methods for critical cleanup on objects that might be part of a cycle — prefer context managers for deterministic cleanup instead of __del__.
a --other--> b
^              |
|__other_______|

Refcount(a) = 1 (from b), Refcount(b) = 1 (from a)
Neither reaches 0 on its own -- cyclic GC required to free both

Core lesson 03

Defining __slots__ = ('x', 'y') tells Python to store those attributes in a fixed, compact structure instead of a per-instance dict, reducing memory use significantly for many instances — at the cost of losing arbitrary new attributes.

By default, every instance carries its own __dict__ to hold attributes, which is flexible but has real memory overhead — noticeable creating millions of small objects. __slots__ tells Python exactly which attribute names are allowed, letting it allocate a fixed-size structure per instance instead of a full dict. The tradeoff: you can no longer set arbitrary new attributes not listed in __slots__, and multiple inheritance gets trickier.

Python example
class PointDict:
    def __init__(self, x, y):
        self.x, self.y = x, y

class PointSlots:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x, self.y = x, y

p = PointSlots(1, 2)
# p.z = 3   # AttributeError -- 'z' not declared in __slots__

What to remember

What does __slots__ do, and why would you use it?

Common footguns

  • Adding __slots__ to a class that's also expected to support dynamic attributes (e.g. monkey-patching in tests) — silently breaks that flexibility.
  • Forgetting __slots__ interactions get tricky across multiple inheritance, since each class's slots layout must be compatible.

Python lab

Browser Python lab

Runtime · idle

Python loads on your first run. Your code stays in this browser.

Best practices

  • Use __slots__ for classes you'll instantiate in large numbers (thousands+) where memory matters.
  • Don't reach for manual gc.collect() calls in normal code — let the automatic cyclic collector do its job.
  • Prefer context managers over __del__ for deterministic cleanup — __del__ timing is not guaranteed.
  • Use sys.getsizeof() and tracemalloc for actual memory profiling instead of guessing.

Apply the concept in Interview practice

LRU CachemediumLeetCode #146 · O(1) get/put

Revisit once more from a memory-discipline angle: bounding a cache's size is exactly the kind of explicit memory-management thinking __slots__ and reference-counting awareness support in production systems.

Open problem

Concept checks

Q01

How does CPython primarily manage memory — and when does it actually free an object?

Q02

If reference counting frees objects immediately, why does Python still need a separate garbage collector?

Q03

What does __slots__ do, and why would you use it?