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 the PyObject).
  • 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:

  1. Small Integer Caching: At startup, CPython pre-allocates an array of PyLongObject structs 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.
  2. 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.
  3. 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.

Display Options
Appearance
Text Size
100%