Memory Model — Names, References, refcount & GC
AdvancedEvery value is a heap object; variables are just names bound to objects. Reference counting frees most objects instantly, the cycle collector handles the rest, and __slots__ shrinks per-instance memory.
Overview
Python has no boxes that hold values — it has objects on the heap and names that point to them. Assignment never copies; it binds another name to the same object, which is why mutating through one name is visible through all of them (the aliasing bug from the Data Structures chapters, now explained at the mechanism level). CPython frees an object the instant its reference count hits zero; a separate generational garbage collector exists solely for reference cycles, which refcounting alone can never reclaim. Knowing this model explains is vs ==, why small integers are shared, why del rarely "frees memory" by itself, and how __slots__ cuts memory 3-5x for millions of small objects.
Names Bind to Objects — id, is, and refcount
a = b copies a reference, never the object. is compares identity (same object), == compares value. CPython caches small ints (-5..256) and interns many strings, so is on numbers/strings is a trap — use == for values.
import sys
a = [1, 2, 3]
b = a # binds b to the SAME list object
b.append(4)
print(a) # [1, 2, 3, 4] — one object, two names
print(a is b, id(a) == id(b)) # True True
# Reference count: object is freed the instant this reaches 0
print(sys.getrefcount(a)) # 3 → a, b, plus the temporary argument
# Small-int caching / string interning — why 'is' lies about values
x, y = 256, 256
print(x is y) # True — cached singleton
x, y = 257, 257
print(x is y) # often False — separate objects, equal values
print(x == y) # True — ALWAYS compare values with ==
# The only correct uses of 'is': None, True, False, sentinels
flag = None
print(flag is None) # ✓ idiomaticReference Cycles, gc, and __slots__
Two objects pointing at each other keep both refcounts above zero forever — the generational GC exists to break exactly these cycles. For classes instantiated millions of times, __slots__ removes the per-instance __dict__ and cuts memory dramatically.
import gc
import sys
class Node:
def __init__(self):
self.ref = None
a, b = Node(), Node()
a.ref, b.ref = b, a # cycle: a→b→a, refcounts never hit 0
del a, b # unreachable, but NOT freed by refcounting
print(gc.collect() > 0) # True — cycle collector reclaims them
# __slots__: fixed attribute set, no per-instance __dict__
class OrderDict:
def __init__(self, oid, amount):
self.oid, self.amount = oid, amount
class OrderSlots:
__slots__ = ("oid", "amount")
def __init__(self, oid, amount):
self.oid, self.amount = oid, amount
d, s = OrderDict(1, 499), OrderSlots(1, 499)
print(sys.getsizeof(d.__dict__)) # ~100 bytes of dict PER instance
# s has no __dict__ at all → 3-5x less memory across 10 lakh orders
# trade-off: cannot add new attributes at runtime
# sys.intern: force-share long repeated strings (log parsing, dedup keys)Key Points to Remember
- 1Variables are names bound to heap objects; assignment never copies
- 2Refcount hits 0 → freed instantly; the generational GC only breaks cycles
- 3is checks identity — use it for None, never for numbers or strings
- 4__slots__ drops the per-instance __dict__: big memory wins for hot classes
Interview Questions
Sign in to ask AriaExplain how CPython decides when to free an object — both mechanisms.
Why does 256 is 256 return True but 257 is 257 often False?
When would you add __slots__ to a class, and what do you give up?
Ask Aria about Memory Model — Names, References, refcount & GC
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.