PyCodeItPython trace & interview prep
← Education Hub|python Guide

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.

PC

PyCodeIt

Python & SQL Technical Interview Platform-GitHub-LinkedIn

July 30, 2026·10 min read

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.

Interactive Trace Block
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.

Interactive Trace Block
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!

Interactive Trace Block
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:

Interactive Trace Block
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().

Interactive Trace Block
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.

Continue learning