Historical Context & Motivation
When programming languages were first designed in the 1950s and 1960s, a variable was simply a named memory slot. You stored a number there, and you could overwrite it with a different number. There was no distinction between changing the slot and changing the value inside it because the slot was the value. As languages grew more sophisticated, designers began to separate these two ideas: a variable could be a name that refers to an object, and the object itself could be either changeable or permanently fixed.
The question that this lesson addresses is deceptively simple: when you write x = [1, 2, 3] and later write x.append(4), did you change x or did you change the list? And what happens when you instead write x = [5, 6, 7]? Python treats these two operations very differently, and understanding why is essential to writing bug-free code.
Core Principles & Definitions
Before we can separate mutability from reassignment, we need to understand three foundational ideas that Python is built upon. Every variable in Python is really a name tag attached to an object living somewhere in memory. Whether you can modify that object depends on the object's type, not on the variable itself.
Object
id()), a type, and a value.Variable (Name)
Mutability
Reassignment (Rebinding)
= to make a variable point to a different object. The old object is unchanged — only the name tag moved. This works for every type, mutable or immutable.Visual Explanation — Names, Objects, and Memory
a.append(4) modifies the list in-place (mutation). The variable a still points to the same object at id 0x001. Right side: writing b = "world" creates a brand-new string object and makes b point to it (reassignment). The old string "hello" is now orphaned.The diagram above captures the essential difference. When you mutate an object, the arrow from the variable to the object stays the same — you are reaching into the box and rearranging its contents. When you reassign a variable, you move the arrow so that it points to an entirely new object, potentially leaving the old object without any reference.
How Python Handles Names and Objects Under the Hood
Python's name-binding model is the key to understanding this distinction. When you type x = 42, Python does two things: it creates (or finds an existing) integer object 42 in memory, and then it adds the name x to the current namespace, pointing to that object. The built-in function id() returns an integer that uniquely identifies the object for its lifetime — think of it like a social security number for data.
Detecting Mutation with id()
If you call id(x) before and after an operation and the number is the same, the object was mutated. If the number changed, the variable was reassigned to a new object. This is the single most reliable way to distinguish the two operations.
Why Immutable Objects Force Reassignment
When you write x = 5 and then x = x + 1, it might look like you changed the number 5 into 6. But integers are immutable — the number 5 cannot become 6. Instead, Python creates a new integer object 6 and rebinds the name x to point to it. The same logic applies to strings: s = s + "!" creates an entirely new string rather than appending to the old one.
Augmented Assignment: The Sneaky Middle Ground
The operator += behaves differently depending on the object's type. For a mutable type like a list, a += [4] calls a.__iadd__([4]), which extends the list in-place (mutation). For an immutable type like a string, s += "!" falls back to s = s + "!", which is pure reassignment. This dual behavior is a common source of bugs.
id(variable) before and after the operation. Same id means mutation; different id means reassignment. This is a powerful debugging technique.Mutable vs. Immutable Types in Python
Python's built-in types fall neatly into two camps. Knowing which camp a type belongs to tells you immediately whether operations on that type will produce mutation, reassignment, or both. The table below classifies the most common types you'll encounter.
| Type | Mutable? | Example Mutation | Reassignment Alternative |
|---|---|---|---|
int | ❌ Immutable | Not possible | x = x + 1 |
float | ❌ Immutable | Not possible | y = y * 2.0 |
str | ❌ Immutable | Not possible | s = s + "!" |
tuple | ❌ Immutable | Not possible | t = t + (4,) |
list | ✅ Mutable | a.append(4) | a = a + [4] |
dict | ✅ Mutable | d["key"] = val | d = {**d, "key": val} |
set | ✅ Mutable | s.add(5) | s = s | {5} |
a and b share a mutable list. Mutating through one name affects both. Right panel: two variables x and y share an immutable string. Reassigning x leaves y untouched.This diagram illustrates the most common bug related to mutability. When two names share a mutable object, any mutation made through one name is visible through the other. With immutable objects, this surprise can never happen because the only way to "change" the value is to reassign, which creates a new object and leaves the original untouched.
Worked Example — Tracing id() Through Code
Let's walk through a short Python program step by step, tracking which operations are mutations and which are reassignments by checking id() at each stage.
colors = ["red", "blue"]. Python creates a list object in memory and binds the name colors to it. Suppose id(colors) returns 140200.colors → ["red", "blue"] | id = 140200palette = colors. No new list is created. The name palette now points to the exact same list object. We can verify: id(palette) also returns 140200, and palette is colors evaluates to True.colors.append("green"). This is mutation — we reached into the list and changed its contents. Check: id(colors) is still 140200. And because palette points to the same object, palette is now ["red", "blue", "green"] too.colors = ["yellow"]. This is reassignment. A brand-new list object is created (say, id 140500), and the name colors is rebound to it. The variable palette still points to the original list at id 140200.colors → ["yellow"] (id 140500) | palette → ["red", "blue", "green"] (id 140200)colors is palette returns False. The two names now point to completely different objects. Any further mutation to one will not affect the other.Side-by-Side — Mutation vs. Reassignment
| Feature | Mutation | Reassignment |
|---|---|---|
| What changes? | The object's internal value | Which object the variable points to |
id() result | Stays the same | Changes to a new id |
| Operator / method | .append(), .update(), index assignment | = (assignment operator) |
| Affects shared references? | Yes — all names see the change | No — only the reassigned name is affected |
| Works on immutable types? | No — raises error or not supported | Yes — always works for every type |
| Common bug risk | Unintended side effects via aliasing | Forgetting the old object is not updated |
Connection to Advanced Topics
Understanding mutability vs. reassignment is the foundation for several advanced Python concepts. When you write functions, this distinction determines whether changes to parameters are visible to the caller. Python uses a mechanism called pass-by-object-reference (sometimes called pass-by-assignment): the function receives a reference to the same object, so mutations inside the function affect the caller, while reassignments inside the function only affect the local name.
| This Lesson | Advanced Topic |
|---|---|
| Mutable default arguments | A common Python gotcha: default mutable arguments (e.g., def f(x=[]):) are shared across all calls, causing surprising mutations. |
| Shallow vs. deep copy | When you need independent copies of mutable objects, you must use copy.copy() or copy.deepcopy() to avoid unintended aliasing. |
| Hashability | Only immutable objects can be used as dictionary keys or set elements because their hash values never change. |
| Thread safety | In concurrent programs, shared mutable objects require locks to prevent race conditions. Immutable objects are inherently thread-safe. |
As you continue learning Python, you'll encounter these topics regularly. Every time you do, the mental model you're building now — names point to objects, and those objects may or may not be changeable in place — will help you reason through what the code actually does.
Practice Problems
n = 10 followed by n = n + 5?a = [10, 20]
b = a
a.append(30) # Line 1
print(b)
a = [99] # Line 2
print(b)result after this code runs? Is each += operation mutation or reassignment?
nums = [1, 2]
backup = nums
nums += [3] # Operation A
word = "hi"
backup_word = word
word += "!" # Operation B
result = (backup, backup_word)add_student(roster, name) that should add a student to a class roster (a list). A classmate suggests two implementations:
Version A: def add_student(roster, name): roster.append(name)
Version B: def add_student(roster, name): roster = roster + [name]
Which version actually modifies the original list that the caller passed in, and why does the other version fail to do so?t = ([1, 2], [3, 4])
t[0].append(99)
print(t)
What will print, and does this violate the immutability of the tuple? Explain the relationship between the tuple's immutability and the mutability of its elements.Lesson Summary
In Python, every variable is a name that refers to an object in memory. Mutation means changing the internal contents of an object in-place — the variable still points to the same object (same id()). Reassignment means making the variable point to a completely different object (new id()). Only mutable types (lists, dicts, sets) support mutation. Immutable types (int, float, str, tuple) can only be "changed" through reassignment.
The practical consequence is that when multiple names share a mutable object (aliasing), mutation through one name is visible through all other names. Reassignment, on the other hand, only moves the one name that was reassigned and leaves every other reference untouched. Knowing this distinction helps you avoid common bugs, write correct functions, and reason about Python code with confidence.