Shallow vs deep copy
Overview
Python variables are labels that point to objects (boxes). Assignment with `=` never copies an object — it attaches a second label to the same box. When you need a real copy, you choose between *shallow* and *deep*. A shallow copy creates a new outer container but keeps the same references to the inner objects. Every built-in shortcut — `list.copy()`, `dict.copy()`, `copy.copy()`, `list[:]`, and `list()` — does a shallow copy: original = [[1, 2], [3, 4]] import copy shallow = copy.copy(original) print(original is shallow) # False — new outer list print(original[0] is shallow[0]) # True — inner list still shared A deep copy, via `copy.deepcopy()`, recursively copies every nested object so nothing is shared: deep = copy.deepcopy(original) print(original[0] is deep[0]) # False — independent inner list Mental model: shallow copy makes a new box whose labels still point to the same inner boxes. Deep copy makes new boxes all the way down. Shallow copy is safe and sufficient when the collection is flat and contains only immutables (strings, ints, tuples of immutables). You need `deepcopy` when the structure contains nested mutable values — lists inside lists, dicts inside dicts — and you want to mutate one copy without affecting the other.
Real-world Python code constantly clones data — duplicating a config dict before overriding keys, snapshotting nested API payloads, or isolating state between test runs. Choosing the wrong copy level is a silent, hard-to-trace bug: you modify what you think is an independent copy and corrupt the original.
Where used: FastAPI, Pydantic, Django, pytest
Why learn this
- Prevents silent data corruption when duplicating config dicts or API response payloads
- Explains why mutating a 'copied' nested list still changes the original — a top interview question
- Required for safely snapshotting state in test fixtures and caching layers
Code walkthrough
import copy
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original[0].append(99)
print(shallow[0])
print(deep[0])
Focus: Observe that `shallow[0]` gets modified along with `original`, whereas `deep` remains fully independent.
Common mistakes
- Assuming assignment copies: Writing `backup = settings` creates a second label for the same dict — every change to `backup` changes `settings`. Use `settings.copy()` or `copy.deepcopy(settings)` to get a real copy.
- Using shallow copy on nested structures: Calling `payload.copy()` on a dict whose values are lists gives a new outer dict, but the lists are still shared. Mutating `payload_copy['items'].append(x)` silently corrupts the original. Use `copy.deepcopy()` when inner values are mutable.
- Forgetting deepcopy handles cycles: `copy.deepcopy()` tracks already-copied objects with a memo dict to avoid infinite recursion on circular references. Rolling your own recursive copy will crash or loop; prefer `deepcopy` for arbitrary nested data.
Glossary
- container
- An object, like a list or dictionary, that holds or stores other objects inside it. Example: `my_list = [1, 2]`.
- snapshotting
- Creating an exact copy or record of the current state of data at a specific moment in time. Example: `backup = copy.deepcopy(data)`.
Recall questions
- What does assignment (`=`) actually do — does it copy the object?
- What is the difference between a shallow copy and a deep copy?
- Name three ways to create a shallow copy of a list.
- When is a shallow copy sufficient, and when do you need deepcopy?
Understanding checks
What is the output of the following code?
[2, 3, 4]
`copy.copy()` creates a shallow copy, meaning the outer list is new but the inner list `[2, 3]` is still shared between `original` and `cloned`.
Which copying method would you use to duplicate a configuration dictionary that contains nested lists, so you can safely mutate the copy's lists without affecting the original?
`copy.deepcopy()`
Since the dictionary contains nested mutable objects (lists), a shallow copy (`dict.copy()`) would still share the inner lists. A deep copy recursively copies all nested objects.
Practice tasks
Deep-clone nested API payload
The `duplicate_order` function creates a shallow copy of the order. Modify it to create a fully independent copy using `copy.deepcopy()` so that appending to the copy's items list doesn't affect the original.
Challenge
Snapshot middleware for request context
Write a function `snapshot_context(ctx)` that receives a nested dict representing a request context (with keys like 'user', 'headers' as a dict, and 'query_params' as a list). It should return a frozen snapshot — a deep copy — so that downstream handlers can mutate their own copy while the original context remains pristine. Demonstrate by modifying the snapshot's 'query_params' and showing the original is unaffected.
Continue learning
Previous: Mutable vs immutable types
Previous: Variables as references
Previous: Slicing
Previous: dict internals & ordering