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.
Aditya Challa4 min read
Intro — what this post promises
Update one field on many dataclass instances via dataclasses.replace vs manual new-instance construction vs copy.copy + mutate. This lab reports ops/s on Linux localhost.
It is not dataclass-vs-slots creation/memory (lab 44). Focus: the replace / update pattern.
Related links:
- dataclass vs slots vs dict localhost lab
- heapq merge vs sorted localhost lab
- islice vs list slice localhost lab
- configparser vs json localhost lab
- html escape vs manual localhost lab
- zlib vs gzip compress localhost lab
- difflib vs set ops localhost lab
- stat vs path stat localhost lab
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. No Docker. Order with 5 fields; also @dataclass(slots=True).
Verdict up front (5000 instances, flip status): manual positional ~6456211 ops/s; manual keyword ~3129734; replace ~900026; copy.copy+mutate ~526727. Prefer replace for clarity when fields evolve; manual wins raw speed if the shape is frozen.
Arms
| Arm | Pattern |
|---|---|
replace(o, status=…) | stdlib update |
Order(…, "paid", …) | manual positional |
Order(order_id=…) | manual keywords |
copy.copy then mutate | shallow copy |
| slots variants | same on slotted class |
| two-field update | replace vs manual |
Lab topology
Script: lab-evidence/110-dataclass-replace-vs-manual/results/run_lab.py.
Lead table — one-field status flip (p50)
| Arm | ops/s |
|---|---|
| manual positional | 6456211 |
| manual keyword | 3129734 |
| manual slots | 3412857 |
| replace (dict dataclass) | 900026 |
| replace (slots) | 527405 |
| copy.copy + mutate | 526727 |
Two-field & hot single
| Arm | ops/s |
|---|---|
| manual two fields | 6107668 |
| replace two fields | 704108 |
| manual hot single | 7033260 |
| replace hot single | 956263 |
Why replace anyway
replace survives field reorders and defaults — you name only what changes. Manual constructors break silently when someone inserts a field. On this box replace is ~7.2× slower than positional manual for a one-field flip; that tax is often fine outside tight inner loops.
Reading it
- Domain models / event sourcing copies →
replace. - Ultra-hot numeric kernels with a stable schema → manual construct may win.
copy.copy+ mutate trailed here and is easy to get wrong with nested mutables.- Slots change allocation; replace still pays helper overhead (lab 44 for create/memory).
Frozen note
With frozen=True, in-place mutation is impossible — replace (or a new construct) is mandatory. The speed story then becomes replace vs manual new, not copy-mutate. Measure that variant if your models are frozen.
Field-count sensitivity
replace inspects the dataclass fields and builds a new instance via the generated constructor. More fields ⇒ more work per call, while a positional manual build always passes every argument explicitly. If your record grows to dozens of columns, re-benchmark — the maintainability case for replace gets stronger even if the relative gap widens.
Event / reducer style
Pipelines that emit “old row → new row” copies map cleanly to replace(old, **changes). That keeps call sites short when only 1–2 keys change across many event types. Micro-optimizing to manual constructors tends to fork a family of nearly identical Order(...) lines that drift.
Pitfalls
- Mutating shared nested objects after shallow copy.
- Hand-rolling constructors that drift from the dataclass definition.
- Using replace in a micro-hot loop without measuring.
- Confusing this with slots-vs-dict creation benches.
Reproduce
Evidence: summary.json, summary.txt.
Limits
One Linux box. Flat string/int fields only. Not frozen=True hash maps. Not pydantic/attrs.
Takeaway
Updating status on 5000 orders: manual ~6456211 ops/s beat replace ~900026 ops/s. Default to dataclasses.replace for maintainability; drop to manual only when profiles prove the gap matters.
Lab evidence
What I found running this
Lab 1 Oct 2026 IST. Python 3.13.5. n=5000: manual_positional 6456211 ops/s; replace 900026; copy_mutate 526727. Not lab 44 create/slots. Affiliates: 0. Evidence: lab-evidence/110-dataclass-replace-vs-manual/.
Related links
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
Plate 76
cmath vs math.hypot Magnitudes: Localhost Lab
Hands-on cmath vs math.hypot magnitude ops lab: real ops/s for abs, polar, and phase, measured on Linux localhost today in this hands-on lab for SREs.
1 Oct 2026