You copy a game board so that you can try a move without changing the real one:
board = [["", ""], ["", ""]]
draft = list(board) # shallow copy
draft[0][0] = "X"
print(board[0]) # ['X', '']
list(board) is a shallow copy, a new outer list that holds the same
inner objects. draft and board share both rows, so the move shows up on
the real board.
A deep copy copies everything inside
copy.deepcopy makes a deep copy, which copies the container and
everything inside it:
import copy
board = [["", ""], ["", ""]]
draft = copy.deepcopy(board)
draft[0][0] = "X"
print(board[0]) # ['', '']
A deep copy does more work than a shallow one, and the cost grows with everything the data holds. Measure it before you deep-copy large data on every request.
The memo keeps shared objects shared
Sometimes two parts of your data share one object on purpose. deepcopy
keeps that sharing in the copy. While it works, it fills a memo, a
dictionary that maps the id of each object it has copied to that
object's copy. When it meets the same object again, it takes the copy from the memo
instead of making a second one:
import copy
shared = [1]
source = {"a": shared, "b": shared}
cloned = copy.deepcopy(source)
print(cloned["a"] is cloned["b"]) # True, one new list under both keys
print(cloned["a"] is shared) # False
loop: list[object] = []
loop.append(loop) # a list that contains itself
copied = copy.deepcopy(loop)
print(copied[0] is copied) # True
The memo also stops deepcopy from copying the list that contains itself
forever. The second time it reaches that list, the memo already holds the
copy.
Each call to deepcopy starts with an empty memo. Two separate calls do
not know about each other, so an object shared between their inputs ends
up as two copies. To copy several pieces as one, create the memo yourself
and pass the same dictionary to every call:
import copy
shared = [1]
memo: dict[int, object] = {}
first = copy.deepcopy({"x": shared}, memo)
second = copy.deepcopy({"y": shared}, memo)
print(first["x"] is second["y"]) # True
Start a new memo for the next copy that is not part of the same set.
Check your understanding
You deep-copy config["a"] and config["b"] in two separate calls, and both hold the same list. Do the copies share one list?
Show answer
Hide answer
No. Each call starts with an empty memo, so each one makes its own copy of the list. Pass one memo dictionary to both calls, and the second call reuses the copy that the first call made.
Some objects cannot be copied
deepcopy returns functions and classes unchanged. It cannot copy modules,
open files, locks or generators, and it raises TypeError for them. A
section of your data that holds a module cannot be deep-copied, but you
can still share it.
When a copy fails, say which part of your data caused it. Catch the
TypeError where you know the section's name, and raise your own error
with from err. The original error then stays attached as __cause__.
The exercise does this.
Copy some parts and share the rest
Most real data needs both. You can share a large read-only table. A list
that the caller will change needs its own copy. Decide for each part, then
test both choices. Check with is that the shared parts are still the same
object. Then change a copied part, and check that the original stays as it
was.
Going further
copy.replace, added in Python 3.13, builds a new dataclass or named tuple
with some fields changed. It does not copy the fields you keep, so the old
object and the new one share a list field:
import copy
from dataclasses import dataclass
@dataclass(frozen=True)
class Options:
retries: int
labels: list[str]
old = Options(2, ["initial"])
new = copy.replace(old, retries=5)
print(new.retries, new.labels is old.labels) # 5 True
frozen=True stops you assigning a new list to labels, but the list
itself can still change. On Python 3.12, use
dataclasses.replace(old, retries=5), which does the same for dataclasses.
A class can define __deepcopy__(self, memo) to control how deepcopy
copies it. Pass memo on to every deepcopy call inside it. See the
copy module documentation.