Skip to content
← Back to explainers

Python explainer

Copy Nested Data with deepcopy

About 4 min to read. Allow extra time to work through examples.

Use copy.deepcopy when a copy must not share its inner objects, pass one memo to keep shared objects shared, and decide for each part of your data whether to copy or share it.

This article uses Python. You can make it your practice mode to browse related content across the site.

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

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.

step 6 of 38

Memory, Copies and Slow Code

Up next Deep-Copy Only the Sections You Choose