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
| Situation | What to Use |
|---|---|
| Simple list/dict, no nesting | .copy() or [:] |
| Nested data structures | copy.deepcopy() |
| Function argument safety | Make a copy at the start |
| Storing history/snapshots | copy.deepcopy() before mutating |
| Read-only data | Use 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 = []