Python • Syntax and Core Types

Mutability vs. Reassignment — Understand mutability vs reassignment (conceptual)

Learn the crucial difference between changing what a variable points to and changing the object itself.

Code braces with terminal and markup symbols representing Python programming.
Inside this lesson 10 sections 5 questions 5 figures

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

John McCarthy's LISP language distinguished between atoms (immutable symbols) and cons cells (mutable pairs), planting the seed for the mutability concept.

1972 • C Separates Variables from Pointers

The C language made explicit that a pointer (a name) and the data it points to are different things, but left almost everything mutable.

1991 • Python Is Born

Guido van Rossum released Python with a clear split: some built-in types (lists, dicts) are mutable, while others (strings, tuples, ints) are immutable. Variables are always rebindable names.

2000s • Functional Programming Revival

Languages like Haskell and Clojure championed immutability by default, showing that avoiding in-place changes reduces bugs. This philosophy influenced modern Python best practices.

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.

1

Object

A chunk of data stored in memory. Every object has an identity (its memory address, checked with id()), a type, and a value.
2

Variable (Name)

A label that refers to (points to) an object. A variable is not a container — it is a reference. Multiple names can point to the same object.
3

Mutability

An object is mutable if its value can be changed in-place after creation (e.g., lists, dicts, sets). An object is immutable if it cannot be changed (e.g., int, float, str, tuple).
4

Reassignment (Rebinding)

Using = 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

Think of variables like sticky notes on boxes. Reassignment is peeling your sticky note off one box and sticking it on a completely different box. Mutation is opening the box your sticky note is already on and rearranging the items inside. The sticky note stays on the same box, but the contents change.

SECTION 3

Visual Explanation — Names, Objects, and Memory

Mutation vs. Reassignment in MemorySTEP 1a = [1, 2, 3]a[1, 2, 3]id: 0x001STEP 2 — Mutationa.append(4)a[1, 2, 3, 4]Same id: 0x001 ✓STEP 1b = "hello"b"hello"id: 0x002STEP 2 — Reassignmentb = "world"b"world"New id: 0x003 ✗"hello" 🗑️(orphaned, may be garbage-collected)MUTATIONSame object, contents changeid(a) stays the sameREASSIGNMENTDifferent object, name re-boundid(b) changesKey test: if id() is the same before and after → mutation. If id() differs → reassignment.
Left side: calling 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

Whenever you're unsure whether an operation mutated or reassigned, print 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.

TypeMutable?Example MutationReassignment Alternative
int❌ ImmutableNot possiblex = x + 1
float❌ ImmutableNot possibley = y * 2.0
str❌ ImmutableNot possibles = s + "!"
tuple❌ ImmutableNot possiblet = t + (4,)
list✅ Mutablea.append(4)a = a + [4]
dict✅ Mutabled["key"] = vald = {**d, "key": val}
set✅ Mutables.add(5)s = s | {5}

Common Python types and their mutability

Shared References: Why Mutation MattersWhen two names point to the same mutable object, mutation affects bothMUTABLE OBJECT (list)ab[1, 2, 3]a = [1,2,3]; b = aAfter a.append(4):[1, 2, 3, 4]b is also [1, 2, 3, 4] ⚠️Both names saw the change!IMMUTABLE OBJECT (str)xy"hi"x = "hi"; y = xAfter x = x + "!":"hi!"y is still "hi" ✓Only x moved to the new object
Left panel: two variables 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. 1

    Step 1 — Create a list and bind a name

    We run 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 = 140200
  2. 2

    Step 2 — Create a shared reference

    We run palette = 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.
    Both names point to id 140200
  3. 3

    Step 3 — Mutate the list through one name

    We run 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.
    id unchanged → Mutation confirmed
  4. 4

    Step 4 — Reassign one name

    We run 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)
  5. 5

    Step 5 — Verify independence after reassignment

    After the reassignment, colors is palette returns False. 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

FeatureMutationReassignment
What changes?The object's internal valueWhich object the variable points to
id() resultStays the sameChanges to a new id
Operator / method.append(), .update(), index assignment= (assignment operator)
Affects shared references?Yes — all names see the changeNo — only the reassigned name is affected
Works on immutable types?No — raises error or not supportedYes — always works for every type
Common bug riskUnintended side effects via aliasingForgetting the old object is not updated

Quick-reference comparison table

✦

KEY TAKEAWAY

Imagine two roommates, Alice and Bob, who share a Google Doc (mutable object). If Alice edits a paragraph, Bob instantly sees the change — that's mutation. Now imagine they each get a printed copy (immutable object). If Alice writes notes on her copy, Bob's copy is completely unaffected — that's like reassignment with immutable types.

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 LessonAdvanced Topic
Mutable default argumentsA common Python gotcha: default mutable arguments (e.g., def f(x=[]):) are shared across all calls, causing surprising mutations.
Shallow vs. deep copyWhen you need independent copies of mutable objects, you must use copy.copy() or copy.deepcopy() to avoid unintended aliasing.
HashabilityOnly immutable objects can be used as dictionary keys or set elements because their hash values never change.
Thread safetyIn 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

Explain in your own words: why can't you "mutate" an integer in Python? What happens instead when you run n = 10 followed by n = n + 5?
Show answer
Integers are immutable in Python, meaning the object representing the number 10 cannot have its internal value changed to 15. When you write 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

Consider the following code. Predict the output and explain whether each numbered line is mutation or reassignment. a = [10, 20] b = a a.append(30) # Line 1 print(b) a = [99] # Line 2 print(b)
Show answer
Line 1 (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

What is the value of 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
Operation A (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

You are writing a function 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
Version A works correctly. The .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

A tuple is immutable, but consider this code: 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
The output is ([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.

Varsity Tutors • Python • Mutability vs. Reassignment