Variables, Mutation & CPython Object Caching
State bugs in Python rarely stem from exotic language features; they stem from misunderstanding object aliasing, mutation, and CPython’s aggressive object caching. In Python, assignment never copies data. It only binds a name to an object reference.
This chapter details reference counting, the difference between mutating a payload versus rebinding a pointer, and how CPython silently caches singletons to save memory.
1. Identity vs. Equality
When dealing with objects, Python relies on two different comparison protocols:
- Equality (
==): Invokes the__eq__dunder method on the object to compare the actual values (the payload of thePyObject). - Identity (
is): Compares the raw C memory addresses (pointers) of the two objects.
list_a = [1, 2, 3]
list_b = [1, 2, 3]
print(list_a == list_b) # True (Values match)
print(list_a is list_b) # False (Distinct memory allocations)2. Visual Mental Model: Aliasing & Rebinding
Assigning a variable to another variable creates an alias—two names pointing to the exact same memory address.
Aliasing (a = b):
[ Symbol Table ]
"list_a" ----> [ PyListObject @ 0x22F4 ] <---- "list_b"
Mutating the shared object (list_a.append(4)):
"list_a" ----> [ PyListObject @ 0x22F4 ] (Now contains [1, 2, 3, 4])
"list_b" ----> (Sees the same mutation)Rebinding changes where a name points, decrementing the ob_refcnt of the old object and incrementing it for the new object.
Rebinding (list_a = ["new"]):
"list_a" ----> [ PyListObject @ 0x99B1 ] (Contains ["new"])
"list_b" ----> [ PyListObject @ 0x22F4 ] (Still contains [1, 2, 3, 4])3. CPython Object Caching (Interning)
To avoid excessive heap allocations for commonly used primitives, CPython implements several internal caches:
- Small Integer Caching: At startup, CPython pre-allocates an array of
PyLongObjectstructs for integers ranging from -5 to 256. Any time your code evaluates to an integer in this range, CPython returns a pointer to the cached singleton rather than allocating a new object. - String Interning: Short strings (especially identifiers like variable names and dict keys) are often “interned” via
PyUnicode_InternInPlace(). Only one instance of an interned string exists in memory. - Empty Tuple Singleton: There is only one empty tuple object
()in the entire VM.
x = 256
y = 256
print(x is y) # True! Pointing to the same cached singleton
a = 257
b = 257
print(a is b) # False! Outside the cache range (in REPL, though compiler optimizations may vary in scripts)4. Garbage Collection: Refcounting & Cyclic GC
CPython relies primarily on Reference Counting (ob_refcnt). When a variable is deleted or goes out of scope, the counter drops. If it hits 0, the C-level memory deallocator (tp_dealloc) is called immediately.
However, reference counting cannot detect circular references (e.g., a list containing itself). To solve this, CPython includes a secondary Generational Cyclic Garbage Collector that periodically scans objects tracked by the PyGC_Head doubly-linked list, resolving isolated cycles.