Python Names, Objects, Binding, Mutation and Rebinding
The Mental Model You Actually Need
Introduction
A common way to think of a Python variable is as a box that stores a value. You write x = 10, and the idea is that Python creates a box called x and puts 10 inside it. It is a useful model for getting started, but it becomes misleading when you need to understand how Python actually handles objects and references.
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4]
print(b) # [1, 2, 3, 4]
If a were simply a box containing the list, changing b should have left a untouched. But it did not.
The better mental model is that Python variables are names bound to objects. a is not a container holding the list. It is a name that refers to a list object. When we write b = a, Python does not create another list. It binds another name, b, to the same object.
So when b.append(4) runs, the list itself is mutated. Since both a and b refer to that same object, the change is visible through either name.
This distinction between names, objects, and references is the foundation for understanding Python's behavior around assignment, mutation, rebinding, identity, and function arguments. Once this mental model is clear, many Python behaviors that initially seem surprising become straightforward to reason about.
How Python Names and Objects Really Work
A useful starting point is to separate names, objects, and references. A name is an identifier in a namespace dictionary that stores a reference (memory address) to a value, and an object is the actual memory allocation containing data, type information, and a reference count that Python evaluates at runtime. A binding connects the name to the object.
Consider:
x = 10
y = {"name": "John"}
z = y
z["age"] = 30
print(y) #{'name': 'John', 'age': 30}
y = {"age": 36}
print(y) #{"age": 36}
When Python executes an assignment such as x = 10, it is useful to think in terms of binding, not storage. Python evaluates the expression on the right, obtains or creates an object, and then binds the name on the left to that object. The same happens with y: the name y is bound to a dictionary object.
Python keeps track of these binding relationships through namespaces, which can be thought of as mappings between names and the objects to which they are bound.
You can access the namespace lists in Python using the
dir(),globals(), andlocals()built-in functions.
The important part happens at z = y. Python does not create another dictionary or copy the existing one. It binds z to the same object that y already refers to.
This means two names can refer to one object. This is known as aliasing. We can observe shared identity with is: y is z returns True.
Now z["age"] = 30 changes the dictionary object itself. Since y and z refer to that same object, the change is visible through both names. This is mutation.
But y = {"age": 36} does something fundamentally different. The original dictionary is not changed. Instead, y is rebound to a different object.
This is where mutation and rebinding become important. Mutating an object changes the object. Rebinding changes what a name refers to. Once those two operations are distinguished, behavior that might otherwise seem surprising becomes much easier to trace. The next step is to understand why some objects can be mutated while others cannot, which brings us to mutable and immutable objects.
Mutable and Immutable Objects
Objects are designed to behave differently when it comes to change. Some can be modified after creation, while others cannot. This distinction is useful because programs need both mutable objects for data that changes and immutable objects for values that should remain stable, with different implications for sharing, performance, and safety.
To say an object is mutable means that the object can be changed after it has been created. A list is a good example:
items = [1, 2, 3]
items.append(4)
The name items still refers to the same list object, but the contents of that object have changed. That is mutation.
An immutable object, on the other hand, cannot be changed after it is created. Common immutable objects include integers, floats, strings, tuples, and booleans. This does not mean they are somehow fixed values stored inside variables. It means that the particular object itself cannot be modified.
With an immutable object, an operation that appears to change the value must instead produce or refer to another object:
a = 10
b = a
a += 5
Here, a += 5 does not modify the integer object 10. Since integers are immutable, a ends up bound to 15, while b continues to refer to 10.
This is why statements such as x += ... can sometimes be misleading if we only look at the syntax. The result depends partly on the object's behaviour. For a mutable object, an augmented operation may modify the existing object. For an immutable object, it generally results in a new object and a rebinding of the name.
In Python, type() tells us an object's type, id() gives us its identity, and is lets us test whether two names refer to the same object:
a = [1, 2]
b = a
c = [1, 2]
print(type(a)) # <class 'list'>
print(a is b) # True
print(a is c) # False
print(a == c) # True
print(id(a)) # 139895621863808
print(id(b)) # 139895621863808
print(id(c)) # 139895621865728
Starting with a = [1, 2], Python creates a list object and binds a to it. When we write b = a, Python does not create another list. It simply binds b to the same object. That is why a is b returns True. Both names refer to the same object.
Now look at c = [1, 2]. Although c refers to a list containing the same values, Python creates a separate list object. Therefore, a is c is False: they are different objects. But a == c is True because the two lists contain equal values.
Note: The
==operator (Equality) compares the actual data or values inside objects, while theisoperator (Identity) compares the memory addresses of two objects (using theid()function). The actual numbers returned byid()are not important and can differ between runs.
id(a) is equal to id(b) because a and b point to the same object in memory, but id(a) is not equal to id(c) because they point to two different memory objects, even though their values are the same.
💡 Python Optimization Note: If you test
a = 10; b = 10; a is b, you will getTrue. This is because Python optimizes memory by caching small integers in the range-5to256and certain strings (known as interning), pointing them to the same pre-allocated memory addresses. If you try the same experiment with larger numbers (e.g.,300), you will seeFalseas expected.
Now we can move to copying. If assigning another name can make two names refer to the same object, the natural question is: how do we create a separate object instead?
Copying Objects in Python
So far, assignment has shown that giving one name to another does not create a new object. If b = a, both names refer to the same object. That is useful when shared state is intentional, but sometimes b needs its own object so that changes to it do not affect a. This is where copying becomes important.
To create a separate object, it must be explicitly copied:
a = [1, 2, 3]
b = a.copy()
b.append(4)
print(a) # [1, 2, 3]
print(b) # [1, 2, 3, 4]
However, copy() creates a shallow copy. It creates a new outer object, but objects nested inside it may still be shared.
a = [[1, 2], [3, 4]]
b = a.copy()
b[0].append(5)
print(a) # [[1, 2, 5], [3, 4]]
The outer lists are different, but the inner list [1, 2] is shared. Changing that inner object therefore affects what is observed through both names.
When the nested objects also need to be independent, a deepcopy() can be used:
import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0].append(5)
print(a) # [[1, 2], [3, 4]]
print(b) # [[1, 2, 5], [3, 4]]
Common Misconceptions and Mistakes
Most confusion around Python variables comes from mixing up names, objects, and bindings. The common mistakes are easier to see once that distinction is clear.
"Variables store values." More accurately, names are bound to objects. This explains why multiple names can refer to the same object.
"
b = acopiesa." It does not. It bindsbto the same object asa. A copy must be explicitly created."Assignment changes an object." Assignment changes a binding. Mutation changes the object.
"Python passes variables by reference." This is an unhelpful description. When a function is called, its parameter is bound to the same object supplied by the caller. Whether the caller observes a change depends on what happens next.
"Equal objects are the same object."
==tests equality;istests identity. Two separate objects can be equal without being the same object."Immutable means the variable cannot change." Immutability describes the object, not the name. A name referring to an integer can still be rebound to another integer.
"
id()determines equality."id()identifies an object. It is not a replacement for==. Use==for equality andiswhen identity matters."
+=always mutates the object." Not necessarily. Its behaviour depends on the object's type. A list can be modified in place, while an integer is immutable, so the name is rebound to another object."A mutable default argument creates a fresh object for every call." It does not. Default values are created when the function is defined, so a mutable default can be shared across calls.
The recurring mistake behind all of these is the same: treating a name as if it were the object itself. A better habit is to ask: What object does this name refer to, and is this operation mutating that object or changing the binding?
Why This Mental Model Matters
The distinction between names, objects, references, mutation, and rebinding becomes increasingly important as Python code gets more complex. It helps you predict not just what individual lines do, but how objects and state behave across a program.
This mental model is particularly useful for:
Functions and shared state: Understanding parameter binding, mutation, and rebinding helps you predict whether a function changes an existing object or only changes its local name.
Copying and data manipulation: Shallow copies, deep copies, and nested structures become easier to reason about when you understand how objects are shared.
Scope and advanced Python: Classes, closures,
global,nonlocal, decorators, callbacks, and late binding all build on the same name–object model.Debugging: When behavior is unexpected, you can trace which objects exist, which names refer to them, and whether an operation mutated an object or rebound a name.
Practical Experiments
The accompanying notebook contains the concepts covered in this article, along with additional tests and experiments that allow each behavior to be observed directly in Python.
Conclusion
Python becomes much easier to reason about once names, objects, and bindings are kept distinct. Assignment binds names to objects, multiple names can refer to the same object, mutation changes an object, and rebinding changes what a name refers to. Mutability, copying, identity, and equality all follow naturally from these relationships. The accompanying notebook provides additional experiments to reinforce these concepts through direct observation.
References
https://docs.python.org/3/reference/executionmodel.html
https://docs.python.org/3/library/copy.html
https://docs.python.org/3/reference/datamodel.html
