Why Shallow Copies Fail on Nested Data
The Mutability Mistakes Killing Your Code | Part 2

When working with nested data structures—lists of dictionaries, dictionaries of lists, trees of mutable objects—a shallow copy does not replicate state. It replicates only the top-level container. The illusion of separation is what makes the resulting bugs subtle, persistent, and difficult to reason about.
To understand why shallow copies fail on nested data, we must first understand what Python actually copies—and what it deliberately does not.
The Illusion of Isolation in Nested Structures
Consider this code. It looks innocent:
a = [[1, 2], [3, 4]]
b = a.copy()
b[0].append(99)
print("a = ",a)
What do you expect? You copied a into b. You modified b. So a should be untouched, right?
Wrong.
a = [[1, 2, 99], [3, 4]]
a changed. You never touched it. And yet there it is — mutated, wearing the evidence of a crime you committed on an entirely different variable.
Welcome to the shallow copy problem.
What's Actually Happening in Memory
To understand why this happens, you need to stop thinking about variables as containers and start thinking about them as labels — sticky notes pointing to locations in memory.
When you write a = [[1, 2], [3, 4]], Python creates three objects:
An outer list
An inner list
[1, 2]An inner list
[3, 4]
The outer list doesn't contain the inner lists. It holds references to them — memory addresses, essentially.
When you call a.copy(), Python creates a new outer list. But it populates that new list with the same references. It copies the sticky notes, not the objects they point to.
So a[0] and b[0] are different labels, but they point to the exact same list in memory. You can verify this yourself:
print(id(a[0]) == id(b[0])) # True
Output:
True
Same object. When you append 99 to b[0], you're modifying the underlying list that both a[0] and b[0] point to. Python isn't hiding this from you — it's just doing exactly what you asked, which wasn't quite what you meant.
This is the shallow copy: a new container, same contents.
The Vocabulary Worth Knowing
Shallow copy — duplicates the outermost container. All nested objects remain shared between the original and the copy. Any mutation to a nested object propagates to both.
Deep copy — recursively duplicates every object at every level of nesting. The copy is entirely independent; no references are shared.
Python's copy module gives you access to both, and the distinction matters more than most tutorials let on.
The Fix
The solution is copy.deepcopy():
import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0].append(99)
print(a) # [[1, 2], [3, 4]] — unchanged
deepcopy walks the entire object graph — every nested list, every nested dict, every nested object — and creates independent duplicates of all of them. b is now a completely separate entity. Mutations to b have no knowledge of a, and vice versa.
Performance: The tradeoff is performance. deepcopy is slower and memory-intensive because it's doing significantly more work. For small data structures, this is irrelevant. For large ones, it's a cost worth weighing.
When Shallow Copy Is Actually the Right Tool
It's tempting to conclude that you should always use deepcopy to be safe. That's the wrong takeaway.
Shallow copy is not a bug — it's a deliberate design choice. There are plenty of situations where shared references are exactly what you want:
Large datasets. If you're working with a list of thousands of objects and you only need to reorder or filter them — not mutate the objects themselves — a shallow copy is far more efficient. You get a new list without duplicating everything inside it.
Performance-critical paths. Deep copying large, nested structures can be orders of magnitude slower. In tight loops or real-time processing, this overhead matters.
When immutability is the better design. If your nested objects are immutable — tuples, strings, frozensets — the question becomes moot. Immutable objects can't be mutated, so whether references are shared is irrelevant. In these cases, shallow copy is perfectly safe and far more efficient.
The real skill isn't knowing that deepcopy exists. It's knowing which one fits the problem you're actually solving.
The Deeper Lesson
This behavior isn't a quirk or an oversight in Python's design. It's a direct consequence of how Python manages memory — everything is an object, and variables are references to objects. Copying a container copies its references, not its contents. That's consistent, predictable, and efficient.
The bug only arises when you carry an implicit mental model — that "copying" means creating something entirely new and independent — into a system that defines copying differently.
Once you've internalized the reference model, shallow copy stops being a trap and starts being a tool. You know when it's safe, you know when it isn't, and you know exactly which line to reach for when you need true independence between your data structures.
import copy
# Shallow: new container, shared contents
b = a.copy() # or list(a), or a[:]
# Deep: new everything, fully independent
b = copy.deepcopy(a)
Understand what each one promises — and holds it accountable to.
Key Takeaways
Shallow copy duplicates the outer container, not the nested objects.
Mutating a nested mutable object affects both the original and the copy.
The issue stems from misunderstanding Python’s reference model.
deepcopy()creates full independence but at a performance cost




