Shallow vs deep copy
copy.copy() makes a new outer object referencing the same nested objects. copy.deepcopy() recursively copies everything. The trap fires when the outer object contains mutables — appending to a nested list mutates both the original and the copy.
The classic shallow-copy trap
import copy
a = [[1, 2], [3, 4]]
b = copy.copy(a)
b[0].append(99)
print(a) # [[1, 2, 99], [3, 4]] ← original mutated too
print(b) # [[1, 2, 99], [3, 4]]
print(a is b) # False ← outer is a new object
print(a[0] is b[0]) # True ← inner list shared
The outer list is fresh, so b.append(...) would only affect b. But b[0] is the same list object as a[0]. Mutating it via .append is visible everywhere it’s referenced.
Detailed comparison
import copy
# Original nested list
original = [[1, 2, 3], [4, 5, 6]]
# Shallow copy
shallow = copy.copy(original)
# Deep copy
deep = copy.deepcopy(original)
# Let's modify the nested list
original[0][0] = 'X'
print("Original:", original) # Original: [['X', 2, 3], [4, 5, 6]]
print("Shallow:", shallow) # Shallow: [['X', 2, 3], [4, 5, 6]]
print("Deep:", deep) # Deep: [[1, 2, 3], [4, 5, 6]]
Key differences:
# Shallow Copy:
# 1. Creates a new object but references the same nested objects
# 2. Only copies the first level of the object
# 3. Changes to nested objects affect both original and copy
list1 = [1, [2, 3]]
list2 = copy.copy(list1)
list1[1][0] = 'changed'
print(list2[1][0]) # Output: 'changed'
# Deep Copy:
# 1. Creates a new object and recursively copies all nested objects
# 2. Creates independent copies at all levels
# 3. Changes to nested objects don't affect each other
list1 = [1, [2, 3]]
list2 = copy.deepcopy(list1)
list1[1][0] = 'changed'
print(list2[1][0]) # Output: 2
When to use which:
# Use shallow copy when:
# 1. Your object contains only immutable objects (numbers, strings, tuples)
numbers = [1, 2, 3]
shallow_numbers = copy.copy(numbers) # Safe because numbers are immutable
# Use deep copy when:
# 2. Your object contains nested mutable objects (lists, dictionaries)
nested_dict = {'a': [1, 2], 'b': [3, 4]}
deep_dict = copy.deepcopy(nested_dict) # Safe for nested structures
Idioms that produce shallow copies
These all behave the same as copy.copy(x) for their respective types:
list2 = list1[:] # slicing
list2 = list(list1) # constructor
list2 = list1.copy() # method
list2 = [*list1] # unpacking
dict2 = dict1.copy()
dict2 = dict(dict1)
dict2 = {**dict1}
set2 = set1.copy()
set2 = set(set1)
set2 = {*set1}
If list1 contains nested mutables, all of the above share them.
When deep copy itself trips you up
import copy
class Connection:
def __init__(self): self.socket = open_real_socket()
data = {"conn": Connection(), "items": [1, 2, 3]}
copy.deepcopy(data) # tries to deepcopy the Connection
# — may fail or duplicate the socket
For objects that aren’t safe to copy (open files, locks, sockets, DB connections), implement __copy__ / __deepcopy__ to return self, raise, or share the resource intentionally. Or: don’t put them in structures you’ll deep-copy.
Interview angle
- Q: “What does
copy.copy([[1, 2], [3, 4]])return, and what happens when you mutateb[0]?” — same outer, shared inner. Mutation visible on both. - Q: “When would you reach for
deepcopy?” — config/cache/fixture defaults with nested mutable state. - Follow-up: “What’s the cost of
deepcopy?” — 10–100× shallow; recursive walk; raises the question of whether values should be immutable instead.
See tricky_questions/48_dict_shallow_copy_trap.md, tricky_questions/01_mutable_default_arguments.md, tricky_questions/44_dict_fromkeys_shared_default.md.