Shallow vs deep copy.
How deep do you need to go?
Assignment doesn't copy in Python — it creates a new reference. To actually copy, you need copy.copy() (shallow) or copy.deepcopy() (deep). The difference matters most with nested structures. A shallow copy of a list of lists shares the inner lists — modifying one via the copy changes the original. A deep copy creates independent inner lists. This page covers the difference, when to use each, and the memory model that makes it all make sense.
Short answer: a = b doesn't copy — it aliases. Use list(a), a[:], or a.copy() for a shallow copy (new container, shared items). Use copy.deepcopy(a) for a deep copy (new container, recursively new items). Shallow is fine for flat containers of immutable items. Deep is needed for nested containers or containers of mutable objects. Assignment, shallow, and deep are the three levels — most bugs come from assuming one when you have another.
✅ The three levels
Assignment, shallow copy, deep copy.
import copy # Level 1: Assignment — no copy at all original = [1, 2, 3] assigned = original assigned.append(4) print(original) # [1, 2, 3, 4] — same object! print(original is assigned) # True # Level 2: Shallow copy — new container, shared items original = [1, 2, 3] shallow = list(original) # or original[:] or original.copy() shallow.append(4) print(original) # [1, 2, 3] — original unchanged print(shallow) # [1, 2, 3, 4] print(original is shallow) # False # Level 3: Deep copy — new container, recursively new items original = [[1, 2], [3, 4]] deep = copy.deepcopy(original) deep[0].append(99) print(original) # [[1, 2], [3, 4]] — inner lists unchanged print(deep) # [[1, 2, 99], [3, 4]] print(original[0] is deep[0]) # False # The shallow copy trap with nested data: original = [[1, 2], [3, 4]] shallow = list(original) shallow[0].append(99) print(original) # [[1, 2, 99], [3, 4]] — modified! print(shallow) # [[1, 2, 99], [3, 4]] print(original[0] is shallow[0]) # True — same inner list!
The three levels:
Assignment (b = a):
→ New name, same object
→ a and b are aliases
→ Modifying one modifies the other
→ id(a) == id(b)
Shallow copy (b = copy.copy(a)):
→ New outer container
→ Same inner items (references shared)
→ Modifying b's items via the container
may affect a (if items are mutable)
→ id(a) != id(b)
→ id(a[0]) == id(b[0]) — same inner object
Deep copy (b = copy.deepcopy(a)):
→ New outer container
→ New inner items, recursively
→ Modifying b never affects a
→ id(a) != id(b)
→ id(a[0]) != id(b[0]) — different inner objects
Memory model:
original = [[1, 2], [3, 4]]
Assignment:
original ──┐
├──→ [ [1, 2], [3, 4] ]
assigned ──┘
(both point to the same list object)
Shallow copy:
original ──→ [ A, B ] where A = [1, 2], B = [3, 4]
shallow ──→ [ A, B ] (new list, same A and B)
Deep copy:
original ──→ [ A, B ]
deep ──→ [ A', B' ] (new list, new A' and B')
Why "shallow" vs "deep":
Shallow goes one level — the outer container is new.
Deep goes all the way — every nested object is new.
What gets deep-copied:
→ Lists
→ Dicts
→ Sets
→ Custom objects (via __deepcopy__ or __dict__)
→ Tuples (if they contain mutable items)
→ Everything, recursively
What doesn't:
→ Immutable objects (int, str, tuple of immutables)
— deepcopy returns the same object
→ Modules, functions, classes
— deepcopy has no concept of "copy a module"
→ File handles, sockets, locks
— deepcopy raises TypeError
For a flat list of ints or strings:
shallow copy is enough.
deep copy works but is slower.
Use shallow.
For a nested list, or a list of dicts, or
a dict of lists:
shallow copy is usually NOT enough.
Use deepcopy.
The rule:
If your structure has any mutable container
inside another container, you probably need
deepcopy.
If everything is immutable (ints, strings,
tuples of immutables), shallow copy is safe.
✅ When each breaks
The bugs, and why they happen.
import copy
# Bug 1: assignment, not copy
original = [1, 2, 3]
copy_wrong = original
copy_wrong.append(4)
print(original) # [1, 2, 3, 4] — bug!
# Fix: use a real copy
original = [1, 2, 3]
copy_right = list(original)
copy_right.append(4)
print(original) # [1, 2, 3] — correct
# Bug 2: shallow copy of nested data
original = [[1, 2], [3, 4]]
shallow = list(original)
shallow[0].append(99)
print(original) # [[1, 2, 99], [3, 4]] — bug!
# Fix: use deepcopy
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0].append(99)
print(original) # [[1, 2], [3, 4]] — correct
# Bug 3: shallow copy of dict with list values
original = {'a': [1, 2], 'b': [3, 4]}
shallow = dict(original)
shallow['a'].append(99)
print(original['a']) # [1, 2, 99] — bug!
# Fix: deepcopy
original = {'a': [1, 2], 'b': [3, 4]}
deep = copy.deepcopy(original)
deep['a'].append(99)
print(original['a']) # [1, 2] — correct
# Bug 4: shallow copy of a list of dicts
original = [{'name': 'Alice'}, {'name': 'Bob'}]
shallow = list(original)
shallow[0]['name'] = 'Charlie'
print(original[0]) # {'name': 'Charlie'} — bug!
# Fix: deepcopy
original = [{'name': 'Alice'}, {'name': 'Bob'}]
deep = copy.deepcopy(original)
deep[0]['name'] = 'Charlie'
print(original[0]) # {'name': 'Alice'} — correct
The four bugs:
Bug 1: Not copying at all
original = [1, 2, 3]
copy = original # same object
copy.append(4) # modifies both
Fix: list(original), original[:], or original.copy()
Bug 2: Shallow copy of nested list
original = [[1, 2], [3, 4]]
copy = list(original) # new outer, same inner
copy[0].append(99) # modifies original[0]
Fix: copy.deepcopy(original)
Bug 3: Shallow copy of dict with list values
original = {'a': [1, 2]}
copy = dict(original) # new dict, same list value
copy['a'].append(99) # modifies original['a']
Fix: copy.deepcopy(original)
Bug 4: Shallow copy of list of dicts
original = [{'name': 'Alice'}]
copy = list(original) # new outer, same dict
copy[0]['name'] = 'X' # modifies original[0]
Fix: copy.deepcopy(original)
The pattern:
All four bugs are variations of the same mistake:
a mutable object is shared between the original
and the "copy."
Rule:
If your structure contains any mutable object
at any depth, use deepcopy.
If everything is immutable, shallow copy is safe.
When shallow is enough:
✅ List of ints: [1, 2, 3]
✅ List of strings: ['a', 'b', 'c']
✅ List of tuples of immutables: [(1, 2), (3, 4)]
✅ Dict with string values: {'a': 'x', 'b': 'y'}
✅ Dict with int values: {'a': 1, 'b': 2}
✅ Set of strings: {'a', 'b'}
These are safe because:
- The items are immutable
- You can't mutate them through the copy
- The only mutations are to the outer container
- Shallow copy gives you a new outer container
When shallow is NOT enough:
❌ List of lists: [[1, 2], [3, 4]]
❌ List of dicts: [{'a': 1}, {'b': 2}]
❌ Dict of lists: {'a': [1, 2]}
❌ Dict of dicts: {'a': {'x': 1}}
❌ List of custom objects with mutable attrs
❌ Any nested mutable structure
The deepcopy cost:
deepcopy is slower than shallow.
It recurses through everything.
For a list of 1000 ints: shallow is microseconds,
deepcopy is milliseconds.
For deeply nested data: deepcopy can be much
slower.
Optimization: shallow copy when you can.
Use deepcopy only when the structure requires it.
If you're copying the same structure many times,
consider whether you need copies at all — often
a functional approach (don't mutate) is better.
The functional alternative:
Instead of copying-then-mutating:
copy = deepcopy(original)
copy[0].append(99)
Build a new structure:
new = [row + [99] if i == 0 else row
for i, row in enumerate(original)]
This creates a new structure without modifying
the original. No deepcopy needed. The downside:
more code for complex transformations.
For simple cases (adding to one inner list),
deepcopy is clearer. For complex transformations,
the comprehension is better. Choose based on
readability, not on avoiding deepcopy at all costs.
✅ Copy methods and recipes
Every built-in copy method.
import copy
# Lists — all shallow copies
lst = [1, 2, 3]
lst.copy() # method (3.3+)
list(lst) # constructor
lst[:] # full slice
copy.copy(lst) # module function
# All equivalent, all shallow.
# Dicts — all shallow copies
d = {'a': 1, 'b': 2}
d.copy()
dict(d)
copy.copy(d)
# Sets — all shallow copies
s = {1, 2, 3}
s.copy()
set(s)
copy.copy(s)
# Tuples — immutable, so copy is the same tuple
t = (1, 2, 3)
copy.copy(t) is t # True — no new object
copy.deepcopy(t) # Same tuple (all immutables)
# Tuple with mutable contents
t = ([1, 2], [3, 4])
copy.copy(t) is t # True — still same tuple
copy.deepcopy(t) # New tuple, new inner lists
# Strings — immutable
s = 'hello'
copy.copy(s) is s # True
# Custom objects — shallow
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Point({self.x}, {self.y})'
p = Point(1, 2)
p2 = copy.copy(p)
p2.x = 100
print(p) # Point(1, 2) — original unchanged (shallow)
print(p2) # Point(100, 2)
# Custom objects — deep
p3 = copy.deepcopy(p)
p3.x = 999
print(p) # Point(1, 2) — original unchanged (deep)
# Custom objects with __dict__ — deepcopy works
class Container:
def __init__(self):
self.items = [1, 2, 3]
c = Container()
c2 = copy.deepcopy(c)
c2.items.append(99)
print(c.items) # [1, 2, 3] — unchanged
print(c2.items) # [1, 2, 3, 99]
# Implementing __copy__ and __deepcopy__
class SmartCopy:
def __init__(self, value):
self.value = value
def __copy__(self):
print('shallow copy called')
return SmartCopy(self.value)
def __deepcopy__(self, memo):
print('deep copy called')
return SmartCopy(copy.deepcopy(self.value, memo))
sc = SmartCopy([1, 2])
copy.copy(sc) # 'shallow copy called'
copy.deepcopy(sc) # 'deep copy called'
Built-in copy methods:
Lists: .copy() list() [:] copy.copy()
Dicts: .copy() dict() copy.copy()
Sets: .copy() set() copy.copy()
All create SHALLOW copies.
All are roughly equal in speed.
Choose for readability:
lst.copy() — clearest intent
list(lst) — most explicit
lst[:] — shortest
copy.copy(lst) — generic
Tuples:
Immutable, so "copy" is usually the same object.
copy.copy(t) is t — True
copy.deepcopy(t) — creates a new tuple only if
contents are mutable
Strings, ints, floats, booleans, None:
Immutable. copy returns the same object.
copy.copy(x) is x — True
Custom classes:
copy.copy(obj) — shallow copy of __dict__
calls __copy__ if defined
copy.deepcopy(obj) — deep copy of __dict__
calls __deepcopy__ if defined
Implementing __copy__ and __deepcopy__:
class MyClass:
def __init__(self, data):
self.data = data
def __copy__(self):
# Return a new instance
# Shallow: share nested objects
return MyClass(self.data)
def __deepcopy__(self, memo):
# Return a new instance
# Deep: recursively copy
return MyClass(copy.deepcopy(self.data, memo))
The memo dict is for tracking already-copied
objects (handles circular references).
Pass it to any deepcopy call inside __deepcopy__.
When to implement __deepcopy__:
→ Your object has resources that shouldn't be
duplicated (file handles, sockets)
→ Your object has custom internal state that
can't be copied by __dict__ alone
→ Your object is expensive to copy and you want
to optimize
→ Your object has circular references that
deepcopy needs help with
When NOT to implement:
→ Default behavior is correct (most cases)
→ You don't have nested mutable state
→ Performance isn't a concern
The default is usually fine.
Implement custom copy methods only when
you have a specific reason.
Special cases:
❌ File handles: deepcopy raises TypeError
(can't copy an open file)
❌ Sockets: deepcopy raises TypeError
❌ Locks (threading.Lock): can't be deep-copied
❌ Modules: deepcopy returns the same module
These are resource objects.
Copying doesn't make sense — you'd get two
handles to the same resource, with undefined
behavior.
If your object holds these, implement
__deepcopy__ to control what gets copied.
Or refactor to keep resources outside the
copied structure.
The cleanest pattern:
Keep mutable state separate from immutable
state when possible.
❌ class Config:
def __init__(self):
self.file = open('config.txt') # resource
self.values = {} # state
✅ class Config:
def __init__(self, values=None):
self.values = values or {}
# File opened per-operation, not stored
Then deepcopy(config) works — it copies values
(a dict, deep-copyable) and nothing else.
The bug is often "I want to copy my object,"
when the real answer is "my object shouldn't hold
that resource."
Don't assume = copies in Python. This is the single most common copy bug, and it shows up in every Python codebase. a = b creates a new name for the same object. If you mutate b, a changes. If you mutate a, b changes. The bug is subtle because simple tests pass — you have to actually mutate the object to notice. And by then, the mutation is in production. The fix is one line: a = list(b) for a shallow copy, a = copy.deepcopy(b) for a deep copy. Use it whenever you're about to mutate a container you received as an argument or returned from a function.
⚠️ Common mistakes
The copy mistakes that cost real time.
= copiesIt aliases. Fix: list(a) or copy.copy(a).
Inner objects are shared. Fix: copy.deepcopy(a).
list(a) on a list of dictsDicts are shared. Fix: copy.deepcopy(a).
Wasted performance. Fix: only deep copy if the structure requires it.
File handles, locks — deepcopy raises. Fix: implement __deepcopy__ or refactor.
memoInfinite recursion if you implement __deepcopy__ wrong. Fix: pass memo to nested deepcopies.
Depends on the copy. Fix: list(d.keys()) or d.copy().
dict.copy() to deep-copy valuesIt's shallow. Fix: copy.deepcopy(d).
❓ Frequently asked
What is the difference between shallow and deep copy in Python?
A shallow copy creates a new container but shares the objects inside. A deep copy creates a new container AND recursively copies everything inside. Use copy.copy() for shallow, copy.deepcopy() for deep. For flat containers of immutable items, a shallow copy is enough. For nested containers or containers of mutable objects, you need a deep copy.
Does assignment (=) copy in Python?
No. Assignment creates a new reference to the same object, not a copy. a = [1, 2, 3]; b = a — a and b point to the same list. Modifying b modifies a. To copy, use copy.copy(a) or list(a) or a[:] for a shallow copy, or copy.deepcopy(a) for a deep copy.
What is the fastest way to copy a list?
Three ways, all creating shallow copies: (1) list(a) — calls the list constructor. (2) a[:] — full slice. (3) a.copy() — the list method. All are equivalent in behavior. For nested lists or lists of mutable objects, none of these are enough — use copy.deepcopy(a).
Why does modifying a copied list change the original?
Because you made a shallow copy, and the inner items are shared. Example: original = [[1, 2], [3, 4]]; copied = list(original). The outer list is new, but the inner lists are the same objects. copied[0].append(99) modifies the inner list in both. Fix: use copy.deepcopy(original).
Does copy.deepcopy handle circular references?
Yes. deepcopy tracks objects it has already copied and reuses the copies, so circular references (an object that references itself, directly or indirectly) are handled correctly. Example: a = []; a.append(a); copy.deepcopy(a) works.
What is copy.copy vs the copy method on lists?
They do the same thing — both create a shallow copy. list.copy() is a method on list. copy.copy(obj) is a function that works on any object with a __copy__ method (or falls back to a shallow copy for built-ins). Use list.copy() when you know it's a list — clearer. Use copy.copy() for generic objects.
🔗 Continue learning
Next in this series: