Skip to content

Assignment vs Mutation

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


Core Difference

Assignment (Rebinding)

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

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:

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:

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

x = "H" + x[1:]     # Must create new string and reassign
print(x)            # "Hello"
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

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

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:

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

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

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

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

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

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

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

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

x = 42
name = "Alice"
pi = 3.14159

Multiple Assignment (Unpacking)

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

Chained Assignment

x = y = z = 0
# All three refer to the same object
print(x is y is z)  # True

Swap Values

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

Index/Slice Assignment

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

Attribute Assignment

class Point:
    pass

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

Common Patterns

Initialize Multiple Variables

# Same value (same object for immutables)
x = y = z = 0

# Different values
x, y, z = 0, 0, 0

Conditional Assignment

# Ternary
result = value if condition else default

# Or pattern (for falsy defaults)
name = user_input or "Anonymous"

Increment/Decrement

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