ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 38

  1. Blog

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 Challa·30 September 2026·4 min read

Summary
On this page
  1. Intro — what this post promises
  2. What each arm guarantees
  3. Lab topology
  4. Lead table — copies/s (p50)
  5. Reading the numbers
  6. Pitfalls
  7. When to pick what
  8. Reproduce
  9. Closing

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

ArmWhat you get
copy.copynew top object; children shared
dict(obj) / list(obj)same shallow contract, often slightly faster
copy.deepcopynew 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

flat dict: N in {16, 128, 1024} string keys, int values
nested dict/list: width 4, depth 2/4/6
list of dicts: 32 or 256 rows x 6 int fields
Each timed call: 200 copies; ops/s from p50 wall

Script: lab-evidence/50-copy-vs-deepcopy/results/run_lab.py.


Lead table — copies/s (p50)

Structurecopy.copydeepcopymanual shallowdeep÷copy
flat dict N=167,997,442175,38712,186,9460.022×
flat dict N=1281,976,16723,6752,170,6330.012×
flat dict N=1024253,9913,130261,1940.012×
nested dict depth 2 w=410,136,334121,63316,656,9550.012×
nested dict depth 4 w=49,920,14649,61315,481,0730.005×
nested dict depth 6 w=410,072,01231,22915,984,6470.003×
nested list depth 2 w=411,578,765205,22921,619,2970.018×
nested list depth 4 w=411,682,24789,44221,083,7010.008×
list of dicts N=328,463,10212,51011,671,3350.001×
list of dicts N=2562,112,2891,5881,775,7260.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() beat copy.copy on these builtins (~1.52× on the small flat dict; ~1.87× on a small nested list). copy.copy pays 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

  1. Shallow copy then mutate a nested list — you mutated the original. deepcopy is the correct tool; just budget it.
  2. Using deepcopy to dodge a shared default argument — fix the default (None then a new dict inside) instead.
  3. Assuming dict(d) deep-copies values — it does not.
  4. Custom objects — this lab is dict/list only. Classes with __deepcopy__ can be faster or slower.

When to pick what

NeedPrefer
New top mapping, children stay shareddict(obj) or copy.copy
Isolate nested mutationsdeepcopy — measure if N is large
One extra key on a request dictshallow merge, not deepcopy
Big payload clone per requestavoid; copy only the fields you change

Reproduce

python3 lab-evidence/50-copy-vs-deepcopy/results/run_lab.py

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.

copy.deepcopycopy.copyshallow copypython copynested dictlocalhost labsreclone

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/.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

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

On this page

  1. Intro — what this post promises
  2. What each arm guarantees
  3. Lab topology
  4. Lead table — copies/s (p50)
  5. Reading the numbers
  6. Pitfalls
  7. When to pick what
  8. Reproduce
  9. Closing
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove