Python Memory Model Deep Dive: Id, Pointers, and Mutability Traps
Understand Python memory pointers, object ids, mutability vs immutability, and how CPython manages memory allocation in code execution.
Understanding the CPython memory model is essential for passing senior Python engineering interviews and building bug-free production systems. In languages like C, variables are names for specific memory addresses that store values directly. In Python, variables are not memory addresses; they are names bound as references (pointers) to heap-allocated objects. Every Python object consists of a standard C header (PyObject) containing at minimum a reference count (ob_refcnt) and a type pointer (ob_type).
1. Variable Assignment Is Name Binding, Not Value Copying
When you execute a = [1, 2, 3] and then b = a, Python does not allocate a new list or copy the elements. Instead, both identifiers 'a' and 'b' point to the exact same PyListObject in memory. The identity function id() reveals the memory address of the object.
a = [1, 2, 3] b = a print(a is b) # True: identical memory pointers print(id(a) == id(b)) # True b.append(4) print(a) # [1, 2, 3, 4] — mutating via 'b' affects 'a'
Because both names point to the same list instance, calling b.append(4) mutates the shared underlying object. Anyone reading 'a' sees the mutation immediately.
2. Integer Interning and the Small Integer Cache
CPython pre-allocates a singleton array of integer objects for small integers ranging between -5 and 256 inclusive. Whenever your program creates an integer in this range, CPython returns a reference to the existing pre-allocated object rather than creating a new PyLongObject on the heap.
x = 256 y = 256 print(x is y) # True: small integer cache singleton x = 257 y = 257 print(x is y) # False: distinct heap objects (in interactive sessions)
For integers outside this range (like 257), CPython creates distinct objects with different memory addresses, so 'x is y' evaluates to False (unless constant folding occurs within the same code block compiler pass). Always compare numerical values using '==' rather than identity operator 'is'.
3. Mutable Objects Inside Immutable Containers
A common technical interview question explores whether tuples are truly immutable. A tuple's immutability guarantees only that its internal array of reference pointers cannot be reassigned, resized, or swapped. However, the objects those references point to retain their own mutability rules!
t = ([1, 2], 3) print(id(t[0])) # Address of the inner list # Modifying the list in-place succeeds: t[0].append(99) print(t) # ([1, 2, 99], 3) # Reassigning the slot directly raises TypeError: # t[0] = [1, 2] # TypeError: 'tuple' object does not support item assignment
The tuple 't' never changed its reference pointers: slot 0 still points to the exact same list object. But because that list is mutable, modifying its elements changes the tuple's apparent contents.
4. The Augmented Assignment (+=) Trap on Tuples
Consider what happens when you run augmented assignment on a mutable item inside an immutable tuple. This is one of the most famous edge cases in Python's execution model:
t = ([1, 2], 3)
try:
t[0] += [4, 5]
except TypeError as e:
print(f"Raised: {e}")
print(t) # What is t[0] now?The code actually mutates the list to [1, 2, 4, 5] AND raises a TypeError! In bytecode, += on a list resolves to the in-place method __iadd__, which successfully extends the list in memory. Afterward, Python attempts to assign the return value back to t[0], which fails because tuple item assignment is prohibited. This dry-run puzzle separates junior candidates from developers with deep CPython mental models.
5. Shallow Copy vs. Deep Copy in Memory
When cloning data structures, slicing or copy.copy() creates a shallow copy: a new outer container whose elements point to the exact same inner objects. Modifying nested mutable objects will leak mutations to the original structure. To completely isolate nested hierarchies, use copy.deepcopy().
import copy original = [[1, 2], [3, 4]] shallow = list(original) # or original[:] deep = copy.deepcopy(original) shallow[0].append(99) print(original) # [[1, 2, 99], [3, 4]] — modified via shallow copy! print(deep) # [[1, 2], [3, 4]] — fully isolated
Quick Concept Quiz
What does the expression 'a is b' evaluate in Python?
Quick Concept Quiz
What happens when you execute t[0].append(10) where t = ([1, 2], 3)?
Ready to test your execution tracing skills?
Hop directly into the Python trace map to start coding, grading queries, and logging XP metrics to your workspace profile.