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.
5 min read
Python's dataclasses.replace(obj, field=value) is the safe way to copy a dataclass with one field changed, but it is slower than building the new instance by hand: updating one field on 5,000 instances it ran about 900k ops/s, against about 6.5M for a manual positional constructor (about 7.2x) and 3.1M for keyword arguments. Use replace by default, because it survives field reorders and new defaults, and switch to a manual constructor only where profiling shows a hot loop paying for it.
It is not dataclass-vs-slots creation/memory (lab 44). Focus: the replace / update pattern.
Related links:
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.
| 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 |
Script: lab-evidence/110-dataclass-replace-vs-manual/results/run_lab.py.
| Arm | ops/s |
|---|---|
| manual positional | 6456211 |
| manual keyword | 3129734 |
| manual slots | 3412857 |
| replace (dict dataclass) | 900026 |
| replace (slots) | 527405 |
| copy.copy + mutate | 526727 |
| Arm | ops/s |
|---|---|
| manual two fields | 6107668 |
| replace two fields | 704108 |
| manual hot single | 7033260 |
| replace hot single | 956263 |
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.
replace.copy.copy + mutate trailed here and is easy to get wrong with nested mutables.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.
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.
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.
Evidence: summary.json, summary.txt.
One Linux box. Flat string/int fields only. Not frozen=True hash maps. Not pydantic/attrs.
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.
It builds a new instance of the dataclass with the fields you name changed and everything else copied, using the generated constructor. You only name what changes.
Compared with a manual constructor, yes: about 900k ops/s vs 6.5M positional for a one-field flip on Python 3.13.5. It still beat copy.copy plus mutate (about 527k ops/s).
Yes, or a new manual construct. With frozen=True you cannot mutate in place, so replace is the standard way to get an updated copy.
Lab evidence
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/.
Explore the product
Untested by us. Check what's included, the license and refund terms before you buy.
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.
Summary1 Oct 20264 min read
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.
Summary1 Oct 20263 min read
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.
Summary1 Oct 20263 min read