Skip to content

Assignment vs Mutation

Understanding the difference between rebinding a name and modifying an object is fundamental to Python.

Mental Model

Assignment moves the name tag to a different object; mutation changes the object the tag is already attached to. Check id() before and after: if it changed, you rebound; if it stayed the same, you mutated.


Core Difference

Assignment (Rebinding)

Assignment creates a new binding from a name to an object:

```python x = [1, 2, 3] print(id(x)) # 140234567890

x = [4, 5, 6] # New binding - different object print(id(x)) # 140234567999 (different!) ```

The original list [1, 2, 3] still exists (until garbage collected), but x no longer refers to it.

Mutation (In-Place Modification)

Mutation modifies the object itself:

```python x = [1, 2, 3] print(id(x)) # 140234567890

x.append(4) # Same object, modified print(id(x)) # 140234567890 (same!) print(x) # [1, 2, 3, 4] ```


Visual Comparison

``` Assignment (x = [4, 5, 6]): Before: x ──→ [1, 2, 3] After: x ──→ [4, 5, 6] (old list orphaned)

Mutation (x.append(4)): Before: x ──→ [1, 2, 3] After: x ──→ [1, 2, 3, 4] (same list, modified) ```


Immutable Types

Immutable types (int, str, tuple, frozenset) cannot be mutated:

```python x = "hello"

x[0] = "H" # TypeError: strings are immutable

x = "H" + x[1:] # Must create new string and reassign print(x) # "Hello" ```

```python x = (1, 2, 3)

x.append(4) # AttributeError: tuple has no append

x = x + (4,) # Must create new tuple print(x) # (1, 2, 3, 4) ```


Mutable Types

Mutable types (list, dict, set) support both mutation and reassignment:

Mutation Methods

python lst = [1, 2, 3] lst.append(4) # [1, 2, 3, 4] lst.extend([5, 6]) # [1, 2, 3, 4, 5, 6] lst.insert(0, 0) # [0, 1, 2, 3, 4, 5, 6] lst.pop() # [0, 1, 2, 3, 4, 5] lst[0] = 99 # [99, 1, 2, 3, 4, 5]

Reassignment

python lst = [1, 2, 3] lst = lst + [4, 5] # New object created lst = [4, 5, 6] # New object created


Why It Matters: Aliasing

When two names refer to the same object, mutation affects both:

```python a = [1, 2, 3] b = a # b refers to same object

b.append(4) # Mutation print(a) # [1, 2, 3, 4] - a sees the change!

b = [5, 6, 7] # Assignment (rebinding) print(a) # [1, 2, 3, 4] - a unchanged ```

Assignment is Safe

```python a = [1, 2, 3] b = a a = [4, 5, 6] # Rebind a to new object

print(b) # [1, 2, 3] - b still refers to original ```

Mutation Affects All References

```python a = [1, 2, 3] b = a a.append(4) # Mutate the shared object

print(b) # [1, 2, 3, 4] - b sees the change ```


Augmented Assignment (+=, *=, etc.)

Augmented assignment behaves differently for mutable vs immutable types:

Mutable (list) — Mutation

```python x = [1, 2] y = x

x += [3, 4] # Calls x.iadd, modifies in place print(y) # [1, 2, 3, 4] - y sees change print(x is y) # True - same object ```

Immutable (int, str, tuple) — Rebinding

```python x = 10 y = x

x += 5 # Creates new int, rebinds x print(y) # 10 - y unchanged print(x is y) # False - different objects ```

+ Always Creates New Object

```python lst = [1, 2] original_id = id(lst)

lst = lst + [3, 4] # Always creates new list print(id(lst) == original_id) # False ```


Function Parameters

Reassignment Inside Function

```python def reassign(lst): lst = [4, 5, 6] # Rebinds local name only

original = [1, 2, 3] reassign(original) print(original) # [1, 2, 3] - unchanged ```

Mutation Inside Function

```python def mutate(lst): lst.append(4) # Modifies the actual object

original = [1, 2, 3] mutate(original) print(original) # [1, 2, 3, 4] - changed! ```


Assignment Examples

Simple Assignment

python x = 42 name = "Alice" pi = 3.14159

Multiple Assignment (Unpacking)

python a, b, c = 1, 2, 3 x, y = "hello", "world" first, *rest = [1, 2, 3, 4] # first=1, rest=[2, 3, 4]

Chained Assignment

```python x = y = z = 0

All three refer to the same object

print(x is y is z) # True ```

Swap Values

python a, b = 10, 20 a, b = b, a print(a, b) # 20, 10

Index/Slice Assignment

python lst = [1, 2, 3, 4, 5] lst[0] = 10 # Index assignment (mutation) lst[1:3] = [20, 30] # Slice assignment (mutation)

Attribute Assignment

```python class Point: pass

p = Point() p.x = 10 # Assigns to object attribute p.y = 20 ```


Common Patterns

Initialize Multiple Variables

```python

Same value (same object for immutables)

x = y = z = 0

Different values

x, y, z = 0, 0, 0 ```

Conditional Assignment

```python

Ternary

result = value if condition else default

Or pattern (for falsy defaults)

name = user_input or "Anonymous" ```

Increment/Decrement

python count = 0 count += 1 # Increment (rebinding for int) count -= 1 # Decrement (rebinding for int)


Quick Reference

Operation Mutation? New Object? Example
x = value No Depends x = [1, 2]
x.append(3) Yes No List method
x.update({}) Yes No Dict method
x += [3] (list) Yes No In-place
x += 1 (int) No Yes Rebinding
x = x + [3] No Yes Always new

Summary

Aspect Assignment Mutation
What changes Name binding Object contents
Object identity May change Stays same
Other references Unaffected See changes
Works on Any type Mutable types only

Key Takeaways:

  • Assignment changes what a name refers to
  • Mutation changes the object itself
  • Aliased names share mutations but not reassignments
  • Augmented assignment (+=) behavior depends on mutability
  • Use id() to check if you have the same object
  • Be careful when passing mutable objects to functions

Exercises

Exercise 1. Predict the output and trace the object identity at each step:

```python a = [1, 2, 3] b = a print(id(a) == id(b))

a = a + [4] print(id(a) == id(b)) print(a) print(b) ```

Now compare with:

python a = [1, 2, 3] b = a a += [4] print(id(a) == id(b)) print(a) print(b)

Why does a = a + [4] give different aliasing behavior than a += [4]?

Solution to Exercise 1

First version (a = a + [4]):

text True False [1, 2, 3, 4] [1, 2, 3]

a + [4] creates a new list object. a = a + [4] rebinds a to this new list. b still refers to the original. Different objects, different ids.

Second version (a += [4]):

text True [1, 2, 3, 4] [1, 2, 3, 4]

a += [4] calls list.__iadd__, which mutates the existing list in place and returns the same object. a and b still refer to the same (now modified) list. Same object, same id.

The difference: a = a + [4] always creates a new object (uses __add__). a += [4] tries __iadd__ first (in-place if mutable). For mutable types, += is fundamentally different from = ... +.


Exercise 2. A function receives a list and is supposed to return a sorted version without modifying the original:

```python def get_sorted(items): items.sort() return items

original = [3, 1, 2] result = get_sorted(original) print(original) print(result) print(original is result) ```

Predict the output. Explain why this function is buggy. What is the correct approach -- and why does understanding assignment vs. mutation matter here?

Solution to Exercise 2

Output:

text [1, 2, 3] [1, 2, 3] True

items.sort() mutates the list in place. Since items and original are aliases for the same list, original is modified. result is also the same object (original is result is True).

The bug: the function was supposed to return a sorted version without modifying the original, but .sort() mutates in place.

Correct approach:

python def get_sorted(items): return sorted(items) # sorted() creates a NEW list

sorted() returns a new list, leaving the original unchanged. Understanding mutation vs. new-object creation is essential for writing functions that do not have unintended side effects on their arguments.


Exercise 3. Explain why the following code produces different results for integers and lists:

```python def increment(x): x += 1

def append_item(lst): lst += [4]

a = 10 increment(a) print(a)

b = [1, 2, 3] append_item(b) print(b) ```

Both functions use +=, but the effect on the caller's variable is different. Why?

Solution to Exercise 3

Output:

text 10 [1, 2, 3, 4]

In increment: x += 1 for an integer calls int.__iadd__, but integers are immutable. This creates a new int object 11 and rebinds the local name x to it. The caller's a is unaffected because rebinding a local name never affects the caller.

In append_item: lst += [4] for a list calls list.__iadd__, which mutates the list in place. The local name lst and the caller's b refer to the same list object. The mutation is visible through b.

The crucial difference: += on immutable types rebinds (local effect only); += on mutable types mutates (visible to all aliases). This asymmetry is one of the most important subtleties in Python's object model.