The copy module provides copy() and deepcopy(). Understanding when to use each prevents subtle bugs with mutable objects.
The Problem
Assignment doesn't copy—it creates another reference:
original = [1, 2, [3, 4]]
reference = original
reference[0] = 999
print(original) # [999, 2, [3, 4]] - original changed!Both variables point to the same object.
Shallow Copy
copy.copy() creates a new container but doesn't copy nested objects:
import copy
original = [1, 2, [3, 4]]
shallow = copy.copy(original)
# Different list object
shallow[0] = 999
print(original) # [1, 2, [3, 4]] - unchanged
# But nested list is shared
shallow[2][0] = 999
print(original) # [1, 2, [999, 4]] - changed!Visualized:
original ──→ [ 1, 2, ─→ [3, 4] ]
↗
shallow ───→ [ 1, 2, ─┘ ]
Deep Copy
copy.deepcopy() recursively copies everything:
import copy
original = [1, 2, [3, 4]]
deep = copy.deepcopy(original)
# Completely independent
deep[2][0] = 999
print(original) # [1, 2, [3, 4]] - unchanged
print(deep) # [1, 2, [999, 4]]Visualized:
original ──→ [ 1, 2, ─→ [3, 4] ]
deep ──────→ [ 1, 2, ─→ [3, 4] ] (separate copy)
When to Use Which
| Situation | Use |
|---|---|
| Flat list/dict (no nesting) | copy() or slice ([:]) |
| Nested structures | deepcopy() |
| Immutable contents only | Assignment is fine |
| Performance critical | copy() if possible |
| Uncertain | deepcopy() is safer |
Built-in Shallow Copy Methods
Many types have built-in shallow copy:
# Lists
new_list = old_list[:]
new_list = list(old_list)
new_list = old_list.copy()
# Dicts
new_dict = dict(old_dict)
new_dict = old_dict.copy()
new_dict = {**old_dict}
# Sets
new_set = set(old_set)
new_set = old_set.copy()All of these are shallow copies.
Common Gotchas
Default Mutable Arguments
# BUG: default list is shared
def append_to(item, target=[]):
target.append(item)
return target
append_to(1) # [1]
append_to(2) # [1, 2] - not [2]!
# FIX: use None and copy
def append_to(item, target=None):
if target is None:
target = []
target.append(item)
return targetClass Attributes
class Team:
members = [] # Shared across all instances!
t1 = Team()
t2 = Team()
t1.members.append("Alice")
print(t2.members) # ["Alice"] - oops
# FIX: initialize in __init__
class Team:
def __init__(self):
self.members = [] # Each instance gets its ownCopying Objects with Circular References
deepcopy handles cycles:
import copy
a = [1, 2]
a.append(a) # Circular reference
b = copy.deepcopy(a) # Works correctly
print(b[2] is b) # True - cycle preserved in copyCustom Copy Behavior
Define __copy__ and __deepcopy__ for custom classes:
import copy
class Config:
def __init__(self, settings):
self.settings = settings
self._cache = {} # Don't copy this
def __copy__(self):
# Shallow copy, fresh cache
new = Config.__new__(Config)
new.settings = self.settings
new._cache = {}
return new
def __deepcopy__(self, memo):
# Deep copy settings, fresh cache
new = Config.__new__(Config)
new.settings = copy.deepcopy(self.settings, memo)
new._cache = {}
return newThe memo dict prevents infinite loops with circular references.
Performance Comparison
import copy
import timeit
data = [list(range(100)) for _ in range(100)]
# Shallow copy - fast
timeit.timeit(lambda: copy.copy(data), number=10000)
# ~0.02 seconds
# Deep copy - slower
timeit.timeit(lambda: copy.deepcopy(data), number=10000)
# ~2.5 secondsdeepcopy is ~100x slower for nested structures. Use it when you need it, not by default.
Practical Example: Undo Stack
import copy
class Editor:
def __init__(self):
self.document = {"title": "", "content": []}
self.history = []
def save_state(self):
# Deep copy to preserve complete state
self.history.append(copy.deepcopy(self.document))
def undo(self):
if self.history:
self.document = self.history.pop()
def add_paragraph(self, text):
self.save_state()
self.document["content"].append(text)
editor = Editor()
editor.add_paragraph("Hello")
editor.add_paragraph("World")
editor.undo()
print(editor.document["content"]) # ["Hello"]Quick Reference
import copy
# Shallow copy (new container, shared contents)
new = copy.copy(original)
# Deep copy (everything is new)
new = copy.deepcopy(original)
# Built-in alternatives (all shallow)
new_list = old_list[:]
new_dict = {**old_dict}
new_set = old_set.copy()| Method | New Container | New Nested Objects |
|---|---|---|
Assignment (=) | No | No |
copy.copy() | Yes | No |
copy.deepcopy() | Yes | Yes |
When in doubt about mutable state: deepcopy. When you know your structure is flat: copy or slicing.