Skip to main content

Copying and Mutability

What You'll Learn​

Why modifying one variable sometimes changes another, the difference between shallow and deep copies, and how to protect your data.

Mutable vs Immutable​

Immutable objects cannot be changed after creation. Every "change" creates a new object:

# Immutable types: int, float, str, bool, tuple, frozenset
x = 10
y = x
x = 20
print(y) # 10 — y is unaffected, x now points to a new object

Mutable objects can be changed in place. Multiple variables can point to the same object:

# Mutable types: list, dict, set
a = [1, 2, 3]
b = a # b points to the SAME list
b.append(4)
print(a) # [1, 2, 3, 4] — a is also changed!

How Assignment Works in Python​

Python variables are labels pointing to objects, not boxes containing values:

a = [1, 2, 3] # 'a' points to a list object in memory
b = a # 'b' points to the SAME list object
c = [1, 2, 3] # 'c' points to a DIFFERENT list object (same content)

print(a is b) # True — same object in memory
print(a is c) # False — different objects
print(a == c) # True — same content

Use id() to see the memory address:

print(id(a)) # e.g. 140234567
print(id(b)) # same number
print(id(c)) # different number

Shallow Copy​

A shallow copy creates a new container, but the elements inside still point to the same objects:

import copy

original = [1, 2, [3, 4]]

# Three ways to shallow copy a list
shallow1 = original[:] # slice
shallow2 = list(original) # list()
shallow3 = original.copy() # .copy()
shallow4 = copy.copy(original) # copy module

# Changing a top-level element is safe
shallow1[0] = 99
print(original) # [1, 2, [3, 4]] — original unchanged

# Changing a nested mutable element affects BOTH
shallow1[2].append(5)
print(original) # [1, 2, [3, 4, 5]] — original changed!
print(shallow1) # [99, 2, [3, 4, 5]]

Deep Copy​

A deep copy creates completely independent copies of everything, recursively:

import copy

original = [1, 2, [3, 4]]
deep = copy.deepcopy(original)

deep[2].append(5)
print(original) # [1, 2, [3, 4]] — unchanged
print(deep) # [1, 2, [3, 4, 5]] — only deep changed

Copying Dictionaries​

user = {
"name": "Alice",
"scores": [90, 85, 92]
}

# Shallow copy — nested list is still shared
shallow = user.copy() # or dict(user)
shallow["name"] = "Bob" # safe — string is immutable
shallow["scores"].append(100)
print(user["scores"]) # [90, 85, 92, 100] — shared!

# Deep copy — completely independent
import copy
deep = copy.deepcopy(user)
deep["scores"].append(100)
print(user["scores"]) # [90, 85, 92] — unchanged

Accidental Mutation — Common Bug​

# ❌ Bug: default mutable argument
def add_item(item, lst=[]): # the list is created ONCE, shared across calls
lst.append(item)
return lst

print(add_item("a")) # ['a']
print(add_item("b")) # ['a', 'b'] — bug! expected ['b']

# ✅ Fix: use None as default
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst

Immutable Containers​

Use frozenset or tuple when you need an immutable version of set/list:

# frozenset — immutable set (can be used as dict key)
immutable_tags = frozenset({"python", "beginner", "tutorial"})
immutable_tags.add("new") # AttributeError — can't modify

# Use tuples for fixed records
point = (10, 20) # immutable pair
rgb = (255, 128, 0) # immutable color

When to Copy​

SituationWhat to Use
Simple list/dict, no nesting.copy() or [:]
Nested data structurescopy.deepcopy()
Function argument safetyMake a copy at the start
Storing history/snapshotscopy.deepcopy() before mutating
Read-only dataUse tuples or frozensets

Real-World Pattern: Processing Without Modifying Original​

import copy

def process_records(records: list[dict]) -> list[dict]:
"""Process records without modifying the originals."""
working_copy = copy.deepcopy(records)

for record in working_copy:
record["name"] = record["name"].strip().title()
record["score"] = round(record["score"], 2)

return working_copy

original_data = [{"name": " alice ", "score": 95.678}]
processed = process_records(original_data)

print(original_data) # [{'name': ' alice ', 'score': 95.678}] — unchanged
print(processed) # [{'name': 'Alice', 'score': 95.68}]

Quick Reference​

# Is it mutable?
# Mutable: list, dict, set
# Immutable: int, float, str, bool, tuple, frozenset

# Check identity (same object)
a is b

# Shallow copy
lst[:] # list slice
list(lst) # new list from list
dct.copy() # dict shallow copy
import copy; copy.copy(obj)

# Deep copy
import copy
copy.deepcopy(obj)

# Safe default argument
def fn(items=None):
if items is None:
items = []

What's Next​

→ Lesson 5: Choosing the Right Container