Variables & Mutation
Variables and mutation are two different ways a running program can appear to change state. Assignment makes a name refer to an object; mutation edits an object that already exists. If two names refer to the same mutable object, both names observe the edit. Reassigning one name does not drag the other name along.
Picture two luggage tags tied to the same suitcase. Changing what is inside the suitcase affects both tag holders; untying one tag and attaching it to another suitcase affects only that tag. Python names work much more like tags than boxes, despite several generations of introductory diagrams insisting otherwise. That is how a helper function can change a list you thought you only lent it.
How variables and mutation work
A Python variable is a name bound to an object. When the program runs a = [1, 2], Python produces a list object and binds a to it. The name does not contain the list. It gives the program a way to reach the list.
The next assignment, b = a, does not copy the list. It binds b to the same object. Now a and b are aliases: two names, one list. Changing the list through either alias changes the single shared object.
That is what b.append(3) does. The binding arrows stay put. The list itself changes from [1, 2] to [1, 2, 3], so reading through a produces the new contents too.
Then b = [9] does something else. Python produces a new list and redirects only b to it. The original list is untouched, and a still reaches it. The distinction is mechanical:
- Mutation: the target object changes; bindings stay put.
- Assignment: a binding changes; existing objects stay as they were.
The syntax alone cannot always tell you which happened. Lists are mutable, but integers are not. In the integer variant, b += 1 looks like an in-place update. Python cannot edit the integer object 7, so it obtains the value 8 as another integer object and rebinds b. a still reads 7.
Variables and mutation Step by Step
The default run uses the mutable-list case.
- The first line creates symbolic object
L1with contents[1, 2]. No name reaches it during this allocation beat. - The same line binds
atoL1. The graph now has one name and one object. b = aadds a second binding toL1. No second list is created. The state isa -> L1andb -> L1.b.append(3)mutatesL1. Its contents become[1, 2, 3]; both bindings remain attached toL1.- Evaluating
[9]creates a different list,L2. For this beat, both names still point toL1because the assignment has not committed yet. - The assignment redirects
bfromL1toL2.astays onL1, whose contents remain[1, 2, 3]. - The final print reads through both bindings and produces
[1, 2, 3] [9] False. - The terminal state keeps both objects on screen:
a -> L1andb -> L2.
The useful visual test is brutally simple. In luggage-tag terms, mutation changes what is inside the suitcase while the tags stay tied; rebinding moves one tag to another suitcase. On the canvas, that becomes "cell changes, arrows stay" followed by "one arrow moves, cells stay." If both happen at once in your mental trace, you have fused two different operations.
Switch to the immutable-int case after the default run. The same alias setup appears, but b += 1 must create the value 8 and redirect b because the object representing 7 cannot be edited.
Why += may rebind instead of mutate
+= is not a promise to mutate. For a built-in list, += extends the same list object. For a built-in integer, the old object cannot change, so Python computes another integer value and rebinds the name. The operator looks the same; the object's type decides the state transition.
Two objects can hold equal values without being the same object. Equality asks whether values compare equal; identity asks whether two names reach the very same object.
This lesson prints a is b only to expose the final binding graph. After b is rebound, the result is False because the two names end at different objects. In ordinary Python code, use == to compare values. Using is for numbers or strings is a classic bug because runtimes may reuse some immutable objects as an implementation detail.
Aliasing matters whenever a function receives a mutable object. If the function appends to a list argument, the caller sees the changed list because both scopes can reach the same object. If the function merely reassigns its local parameter, the caller's binding is untouched. This explains a surprising amount of Python behavior without invoking magic or "pass by reference" as a vague incantation.
Edge Cases
- Two names may refer to one mutable object. Mutating through either name is visible through the other.
- Rebinding the last name that reaches an object can make the object unreachable. Python may then reclaim it, but reclamation is separate from assignment.
- Immutable objects such as integers, strings, and tuples cannot be changed in place. An update-looking expression may calculate a new value and rebind the name.
- A tuple is immutable, but it can contain a mutable list. The tuple's slots stay fixed while the list inside one slot can still change.
a == banda is banswer different questions: equal value versus identical object.
Common Mistakes
- Drawing each variable as a box that owns its value. That model breaks as soon as two names alias the same list; use luggage tags tied to objects instead.
- Assuming
b = acopies a mutable object. Use an explicit copy operation when you need independent state. - Treating
+=as guaranteed mutation. Its behavior depends on the object's type and implementation of the operation. - Using
isfor value comparison. Identity is appropriate for sentinels such asNone; ordinary values usually need==. - Saying a function "changed the variable" without naming which thing changed. Ask the sharper question: did a binding move, or did an object mutate?
A Note on Simplification
The symbolic IDs L1, L2, I7, and I8 are teaching labels, not memory addresses. Real Python runtimes manage allocation, integer interning, reference counts or other garbage-collection machinery, and implementation-specific object layouts. Those details affect storage and lifetime, but they do not change the binding rule taught here: names refer to objects, assignment changes bindings, and mutation changes mutable objects.
Keep exploring
A few lessons that share the same technique or lead into the next idea.
Found a mistake or something unclear? Leave feedback.