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 copying. A shallow copy creates a new outer container but keeps the same references to the inner objects. Every built-in shortcut performs a shallow copy: - `list.copy()`: Duplicates the `list` elements into a new `list`. Returns a new `list` object, raising a `TypeError` if any arguments are passed. - `dict.copy()`: Duplicates the `dict` at the top level. Returns a new `dict`, raising a `TypeError` if any arguments are passed. - `copy.copy(obj)`: The generic shallow copy function from the `copy` module. Returns a shallow clone of the argument, raising a `TypeError` if the object cannot be pickled and lacks copying methods. - `list[:]`: Uses slice notation to duplicate a sequence. Returns a new `list` object, raising a `TypeError` if the object does not support slicing. 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. Mental model: a shallow copy makes a new box whose labels still point to the same inner boxes, while a deep copy makes new boxes all the way down. deep = copy.deepcopy(original) print(original[0] is deep[0]) # False — independent inner list Shallow copying is safe and sufficient when the collection is flat and contains only immutables like strings, `int` values, or tuples of immutables. You need `copy.deepcopy()` when the structure contains nested mutable values, like lists inside lists, and you want to mutate one copy without affecting the other. If a nested object holds a system resource like a file handle or network socket, `copy.deepcopy()` will crash. import copy f = open("test.txt", "w") nested = {"file": f} # copy.deepcopy(nested) # TypeError: cannot pickle '_io.BufferedWriter' object It fails because system resources cannot be meaningfully duplicated in memory. This rule is covered fully in `magic-methods`, where you learn to define custom `__deepcopy__` behavior. Python optimizes copying for immutable containers like `tuple` or `frozenset`. A shallow copy of a `tuple` always returns the exact same object. t = (1, [2, 3]) t_shallow = copy.copy(t) t_deep = copy.deepcopy(t) print(t is t_shallow) # True — tuples are immutable print(t is t_deep) # False — contains a mutable list A deep copy will only create a new `tuple` if it detects a mutable object hidden inside.
Real-world Python code constantly clones data. Common use cases include duplicating a configuration `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 might modify what you think is an independent copy, but you actually corrupt the original data.
Where used: FastAPI, Pydantic, Django, pytest
Why learn this
- Prevents silent data corruption when duplicating config `dict` structures 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, so always prefer `copy.deepcopy()` for arbitrary nested data.
- Attempting to deepcopy unpicklable objects: Calling `copy.deepcopy()` on objects containing open file handles, database connections, or threading locks will raise a `TypeError`. You must either filter out these objects or implement a custom `__deepcopy__` method.
Glossary
- container
- An object, like a `list` or `dict`, 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—
- What happens when you use `copy.deepcopy()` on a `tuple`?
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 `dict` that contains nested `list` objects, so you can safely mutate the copy's nested `list` objects without affecting the original?
`copy.deepcopy()`
Since the `dict` 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. It will contain 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 objects
Previous: Object identity: id() and memory references
Previous: Lists: indexing, slicing, methods
Previous: Dictionaries: operations and use cases