Plate 38
copy vs deepcopy vs manual: Shallow Copy Lab
Hands-on copy.copy vs deepcopy vs manual shallow lab: real ops/s on nested dicts and lists of several sizes, measured on Linux localhost only (eng lab).
Aditya Challa4 min read
Intro — what this post promises
copy.copy vs copy.deepcopy vs a one-liner dict(obj) / list(obj): folklore says deepcopy is “slow” without saying how slow as nesting grows. This lab times all three on flat dicts, nested dicts/lists, and lists of dicts on Linux localhost.
Related links:
- set vs list membership localhost lab
- string concat vs join localhost lab
- sorted vs heapq vs bisect localhost lab
- dataclass vs slots vs dict localhost lab
- json vs orjson vs msgpack localhost lab
- pathlib vs os.path localhost lab
- python re vs str methods localhost lab
- tempfile NamedTemporaryFile localhost lab
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. Manual arm is one-level (dict() / list()), not a hand-rolled recursive clone. No custom __deepcopy__.
Verdict up front: shallow copy stays in the millions of ops/s until the top container is large. deepcopy is the cliff: flat dict N=16 is already ~0.022× copy (175k vs 8.00M/s). List of 256 dicts: deepcopy ~1,588/s vs copy ~2.11M.
What each arm guarantees
| Arm | What you get |
|---|---|
copy.copy | new top object; children shared |
dict(obj) / list(obj) | same shallow contract, often slightly faster |
copy.deepcopy | new tree; mutating a child does not touch the original |
If you only needed a new top dict so you can add a key, deepcopy is paying for isolation you did not use.
Lab topology
Script: lab-evidence/50-copy-vs-deepcopy/results/run_lab.py.
Lead table — copies/s (p50)
| Structure | copy.copy | deepcopy | manual shallow | deep÷copy |
|---|---|---|---|---|
| flat dict N=16 | 7,997,442 | 175,387 | 12,186,946 | 0.022× |
| flat dict N=128 | 1,976,167 | 23,675 | 2,170,633 | 0.012× |
| flat dict N=1024 | 253,991 | 3,130 | 261,194 | 0.012× |
| nested dict depth 2 w=4 | 10,136,334 | 121,633 | 16,656,955 | 0.012× |
| nested dict depth 4 w=4 | 9,920,146 | 49,613 | 15,481,073 | 0.005× |
| nested dict depth 6 w=4 | 10,072,012 | 31,229 | 15,984,647 | 0.003× |
| nested list depth 2 w=4 | 11,578,765 | 205,229 | 21,619,297 | 0.018× |
| nested list depth 4 w=4 | 11,682,247 | 89,442 | 21,083,701 | 0.008× |
| list of dicts N=32 | 8,463,102 | 12,510 | 11,671,335 | 0.001× |
| list of dicts N=256 | 2,112,289 | 1,588 | 1,775,726 | 0.001× |
Shallow copy of a nested dict stays ~10M/s at depth 6 because it does not walk children. Deepcopy falls from ~122k/s (depth 2) to ~31.2k/s (depth 6). That is the whole lesson: depth is free for copy and expensive for deepcopy.
Reading the numbers
- Manual
dict()/list()beatcopy.copyon these builtins (~1.52× on the small flat dict; ~1.87× on a small nested list).copy.copypays a dispatch layer; the result is still shallow. - Flat dict N=1024: copy 253,991/s vs deepcopy 3,130/s (0.012×). Even scalar values are visited by deepcopy’s memo machinery.
- List of dicts N=256: deepcopy 1,588/s (~0.63 ms/copy). That is a request-path hazard if you clone a payload “just in case.”
Pitfalls
- Shallow copy then mutate a nested list — you mutated the original. deepcopy is the correct tool; just budget it.
- Using deepcopy to dodge a shared default argument — fix the default (
Nonethen a new dict inside) instead. - Assuming
dict(d)deep-copies values — it does not. - Custom objects — this lab is dict/list only. Classes with
__deepcopy__can be faster or slower.
When to pick what
| Need | Prefer |
|---|---|
| New top mapping, children stay shared | dict(obj) or copy.copy |
| Isolate nested mutations | deepcopy — measure if N is large |
| One extra key on a request dict | shallow merge, not deepcopy |
| Big payload clone per request | avoid; copy only the fields you change |
Reproduce
Evidence: /workspace/lab-evidence/50-copy-vs-deepcopy/results/.
Closing
Shallow is cheap; deep walks the tree. On this box a 16-key dict is ~8.00M copy.copy/s vs ~175k deepcopy. Depth 6 nested dict: copy stays ~10.1M/s while deepcopy drops to ~31k. A 256-row list of dicts deepcopy is ~1,588/s. Use deepcopy when isolation is the requirement — not as a default clone.
Lab evidence
What I found running this
Lab 1 Oct 2026 IST. Python 3.13.5. flat dict N=16: copy 8.00M/s; deepcopy 175k (0.022x); manual 12.19M. N=1024: copy 254k; deepcopy 3.13k (0.012x). nested dict d6: copy 10.1M vs deepcopy 31.2k. list-of-dicts n=256: deepcopy 1588/s vs copy 2.11M. Affiliates: 0. Evidence: lab-evidence/50-copy-vs-deepcopy/.
Related links
Plate 57
dataclasses.replace vs Manual Copy: Localhost Lab
Hands-on dataclasses.replace vs manual copy/new-instance lab: real ops/s updating one field, measured on Linux localhost in this hands-on lab for SREs.
1 Oct 2026
Plate 17
platform vs os.uname Inventory: Localhost Lab
Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.
1 Oct 2026
Plate 75
uuid.uuid4 vs uuid.uuid1: Localhost Lab
Hands-on uuid.uuid4 vs uuid.uuid1 ID generation lab: real ops/s plus version/node checks, measured on Linux localhost today in this hands-on lab for SREs.
1 Oct 2026