SECTION 1
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.
1958 • LISP Introduces Mutable & Immutable Data
1972 • C Separates Variables from Pointers
1991 • Python Is Born
2000s • Functional Programming Revival
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.
SECTION 2
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.KEY TAKEAWAY
SECTION 3
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.
SECTION 4
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.
Pro Tip
id(variable) before and after the operation. Same id means mutation; different id means reassignment. This is a powerful debugging technique.SECTION 5
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} |
Common Python types and their mutability
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.
SECTION 6
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.
Tracing Mutation vs. Reassignment
- 1
Step 1 — Create a list and bind a name
We runcolors = ["red", "blue"]. Python creates a list object in memory and binds the namecolorsto it. Supposeid(colors)returns140200.colors → ["red", "blue"]| id = 140200 - 2
Step 2 — Create a shared reference
We runpalette = colors. No new list is created. The namepalettenow points to the exact same list object. We can verify:id(palette)also returns140200, andpalette is colorsevaluates toTrue.Both names point to id 140200 - 3
Step 3 — Mutate the list through one name
We runcolors.append("green"). This is mutation — we reached into the list and changed its contents. Check:id(colors)is still140200. And becausepalettepoints to the same object,paletteis now["red", "blue", "green"]too.id unchanged → Mutation confirmed - 4
Step 4 — Reassign one name
We runcolors = ["yellow"]. This is reassignment. A brand-new list object is created (say, id140500), and the namecolorsis rebound to it. The variablepalettestill points to the original list at id 140200.colors → ["yellow"](id 140500) |palette → ["red", "blue", "green"](id 140200) - 5
Step 5 — Verify independence after reassignment
After the reassignment,colors is palettereturnsFalse. The two names now point to completely different objects. Any further mutation to one will not affect the other.Reassignment severed the shared reference.
SECTION 7
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 |
Quick-reference comparison table
KEY TAKEAWAY
SECTION 8
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. |
How this concept connects to what's ahead
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.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
n = 10 followed by n = n + 5?Show answer
n = n + 5, Python creates a brand-new integer object with the value 15 and rebinds the name n to point to it. The original integer 10 still exists unchanged (until Python garbage-collects it). This is reassignment, not mutation, which you could confirm by checking that id(n) changes after the operation.Question 2: BASIC
a = [10, 20]
b = a
a.append(30) # Line 1
print(b)
a = [99] # Line 2
print(b)Show answer
a.append(30)) is mutation. Both a and b refer to the same list object, so the first print(b) outputs [10, 20, 30]. Line 2 (a = [99]) is reassignment. It creates a new list and rebinds a to it, but b still points to the original list. The second print(b) still outputs [10, 20, 30].Question 3: INTERMEDIATE
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)Show answer
nums += [3]) is mutation because lists implement __iadd__, which extends the list in-place. So backup is now [1, 2, 3]. Operation B (word += "!") is reassignment because strings are immutable and don't support in-place modification. A new string "hi!" is created and word is rebound. backup_word is still "hi". Therefore result is ([1, 2, 3], "hi").Question 4: APPLIED
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?Show answer
.append() method mutates the list object in-place, and since the function receives a reference to the same object the caller passed, the change is visible outside the function. Version B does not work as intended. The line roster = roster + [name] creates a brand-new list and reassigns the local variable roster to point to it. This reassignment only affects the local name inside the function — the caller's original list is unchanged. This is a classic example of why understanding mutation vs. reassignment matters for function design.Question 5: CRITICAL THINKING
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.Show answer
([1, 2, 99], [3, 4]). This does not violate the tuple's immutability. The tuple is immutable in the sense that you cannot change which objects it contains — you cannot add, remove, or swap its elements. However, the tuple holds references to objects, and if those objects happen to be mutable (like lists), their internal contents can still change. The tuple's first slot still points to the exact same list object (same id()); it's the list that was mutated, not the tuple. Think of it like a locked display case containing an open notebook — you can't swap the notebook for something else, but you can still write on its pages.SUMMARY
Lesson Summary
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.