Plate 43
copy.copy vs dict.copy Shallow: Localhost Lab
Aditya Challa4 min read
Intro — what this post promises
Shallow-copy dicts and lists with dict.copy() / list[:] vs copy.copy. This lab reports ops/s on Linux localhost, plus a shared-inner correctness check.
Related links:
- itertools batched vs chunk localhost lab
- exitstack vs nested with localhost lab
- chainmap vs dict merge localhost lab
- ipaddress vs string prefix localhost lab
- fractions vs float localhost lab
- math fsum vs sum localhost lab
- xml etree vs json localhost lab
- logging formatter vs fstring localhost lab
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. Differentiates from copy-vs-deepcopy (lab 50) — no deepcopy arm; focus is shallow method vs copy.copy dispatch.
Verdict up front (n=20000 copies of a 200-key dict / 200-elem list): dict.copy ~1890116 ops/s; copy.copy(dict) ~1571728; dict spread ~1804957; list[:] ~4222429; copy.copy(list) ~3346932.
Arms
| Arm | Pattern |
|---|---|
d.copy() | dict method |
copy.copy(d) | generic shallow |
| dict unpack merge | dict spread copy |
dict(d) | constructor copy |
lst[:] | list slice |
copy.copy(lst) | generic shallow list |
Seven rounds, p50. Inner mutable shared across keys so we can prove shallowness.
Lab topology
Script: lab-evidence/126-copy-copy-vs-dict-copy/results/run_lab.py.
Lead table (p50 ops/s)
| Arm | ops/s |
|---|---|
| dict.copy | 1890116 |
| copy.copy(dict) | 1571728 |
| dict spread | 1804957 |
| dict(d) | 1511895 |
| list[:] | 4222429 |
| copy.copy(list) | 3346932 |
Typed methods win: dict.copy edged copy.copy; list[:] led list arms.
Shallowness check
Both d.copy() and copy.copy(d) produced new outer objects (is not d) while sharing the nested dict (inner_shared=True for both). Mutating that inner still aliases — that is the shallow contract, not a bug.
When copy.copy still wins
Polymorphic helpers that accept dict or list or user types need copy.copy (or the copy protocol). Hot dict-only paths should call d.copy() and skip the generic dispatch. Lab 50 remains the place for deepcopy cost.
Reading it for SRE work
- Config snapshot before mutate →
d.copy()(fastest dict arm here). - List buffer fork →
lst[:]. - Generic util →
copy.copy; accept the small tax (~1571728 vs ~1890116). - Nested mutables → either deepcopy (lab 50) or explicit nested copies — shallow will bite.
Method vs generic dispatch
On this box the gap is real but not dramatic for dicts: method ~1890116 ops/s vs copy.copy ~1571728. Spread landed close to dict.copy (~1804957) — fine for clarity, not a free lunch over .copy(). Lists show a wider slice-vs-copy.copy spread (~4222429 vs ~3346932).
If a profiler points at shallow copies in a hot loop, swap copy.copy(x) for the typed method before reaching for deepcopy or JSON round-trips.
Spread and constructor peers
dict(d) lagged at ~1511895 ops/s behind d.copy() ~1890116. Prefer the method in hot paths; keep constructor copies for readability in cold setup code. Remember: none of these arms isolate nested mutables — lab 50 still owns that comparison.
Pitfalls
- Assuming shallow copy deep-clones nested lists/dicts.
- Using
copy.deepcopy“just in case” without measuring (see lab 50). - Comparing to dict merge that intentionally overrides keys.
- Forgetting user classes need
__copy__forcopy.copy.
Reproduce
Evidence: summary.json, summary.txt.
Limits
One Linux box. Builtin dict/list only. Not copy.deepcopy, not third-party containers.
Takeaway
Prefer dict.copy (~1890116 ops/s) and list[:] (~4222429) for typed shallow forks; use copy.copy when the type varies. Inner objects stay shared — measure deepcopy separately when you need isolation.
Lab evidence
What I found running this
Lab 1 Oct 2026 IST. Python 3.13.5. n=20000: dict.copy 1890116 ops/s; copy.copy(dict) 1571728; list[:] 4222429. Shallow only (not lab 50). Affiliates: 0. Evidence: lab-evidence/126-copy-copy-vs-dict-copy/.