4 min read
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:
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.
| 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.
Script: lab-evidence/126-copy-copy-vs-dict-copy/results/run_lab.py.
| 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.
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.
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.
d.copy() (fastest dict arm here).lst[:].copy.copy; accept the small tax (~1571728 vs ~1890116).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.
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.
copy.deepcopy “just in case” without measuring (see lab 50).__copy__ for copy.copy.Evidence: summary.json, summary.txt.
One Linux box. Builtin dict/list only. Not copy.deepcopy, not third-party containers.
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
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/.