A Deep Dive into Python's Object Model and Mutability
Unpacking mutable, immutable, and why variables are never just boxes in Python.

Stock photo for illustration only, not from the actual event
- In Python, variables are never boxes; they are names pointing to objects in memory.
- Mutating a shared mutable object (like a list) affects everyone sharing it.
- Rebinding a variable name (e.g., n = ...) only affects that local name.
- CPython pre-allocates and caches integers in the range -5 to 256.
In most programming languages, you learn early on to separate values from variables: a variable is a labelled box, and you drop a value inside it. Python quietly breaks that mental model. In Python, everything is an object — integers, strings, lists, functions, even classes themselves — and a variable is never the box. It is a name that points at an object living somewhere in memory.
That single idea explains a surprising number of wait, what? moments: why changing one list seems to change another, why a is b is sometimes True and sometimes False for equal values, and why a function can modify your list but not your integer. Let us unpack it.
Every object in Python carries two things we can always ask about: what it is and where it is. This is the distinction that trips everyone up.
When an immutable argument is passed, the caller remains completely unaffected, as demonstrated below.
- The increment function rebinds the local name n to a new integer object.
- The caller's original object a is left entirely untouched.

Stock photo for illustration only, not from the actual event
On the flip side, when a mutable argument is mutated in place, the caller sees the change immediately.
- Calling the .append method mutates the exact same list object.
- The list l passed into the function now reflects the appended value [1, 2, 3, 4].
However, if a mutable argument is passed but its name is rebound inside the function, the caller is once again unaffected.
- Reassigning n = v inside the assign_value function rebinds only the local name.
- The list l1 remains unchanged because we only moved a local reference name.
The rule of thumb is clear: mutating an object (calling a method like .append, or performing index assignment) affects everyone who shares it, whereas rebinding a name (n = ...) affects only that specific name.
Understanding pass-by-assignment in Python is crucial for avoiding subtle bugs related to aliasing. When multiple names reference the same underlying mutable structure, modifying the data through one reference inadvertently alters all of them. Differentiating between in-place mutation and name rebinding separates competent Python developers from beginners.
This is where identity gets genuinely surprising. The size of the integer cache is defined in CPython's source files under Objects/longobject.c by two constants, which together cache integers in the range of -5 to 256, accounting for a total of 5 + 257 = 262 pre-allocated integer objects.
Never rely on this behavior in your production code. The small-integer cache is merely an implementation detail of CPython. Always use == to compare values and reserve is strictly for identity checks such as x is None.
Tuples and frozensets are immutable containers, meaning you cannot add, remove, or replace their elements. Yet there is a subtle catch: the immutability of a container does not guarantee the immutability of its contents. A tuple can hold a mutable object, and that inner object can still change over time. Also, keep note of empty-tuple singletons and single-element quirks.
Keep using == for values and is for identity, and you will write code that behaves exactly the way you expect.
Source: Dev.to
Found something wrong in this article? Report an issue with this article
Comments
Leave a Comment