Plate 80
dataclass vs slots vs dict: Python Object Lab
Hands-on Python dataclass vs slots vs dict lab: instantiation and attribute ops/s plus RSS for classic, slots, dataclass, namedtuple, dict on localhost.
Aditya Challa6 min read
On this page
- Intro — what this post promises
- Types under test
- Lab topology
- Lead table — instantiation (p50 ops/s)
- Lead table — attribute access (p50 ops/s)
- Memory — 200k objects
- How to read these numbers
- Pitfalls we hit (or avoided)
- Practical checklist
- Methodology footnote
- Versions pinned
- When dict still wins
- slots construct tax
- Ratio cheatsheet (vs classic)
- Bottom line for API design
- Verdict
Intro — what this post promises
Should that record be a dict, a dataclass, a slotted class, or a namedtuple? Folklore says slots save RAM and dicts are slow. This lab times instantiation and attribute access, then estimates bytes per object.
This is a hands-on lab with measured numbers:
- Construct ops/s for classic class,
__slots__,@dataclass,@dataclass(slots=True),namedtuple,dict,SimpleNamespace. - Attribute / key read ops/s (
a+b). - Allocate 200k objects — tracemalloc peak + VmRSS delta.
Related links:
- json vs orjson vs msgpack localhost lab
- fork COW RSS vs spawn localhost lab
- python re vs str methods localhost lab
- process vs thread pool GIL localhost lab
- Why your average latency graph is lying (p50 / p95 / p99)
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Four fields (int, int, str, str). Affiliates: 0. Microbench — not a pydantic/ORM study.
Verdict up front: attr access is nearly tied (~20.4–20.9M/s for class/dataclass/slots). Memory is where slots win (~106–212 B/obj vs dict ~295, classic ~425). Instantiation: dict/dataclass slightly beat classic; namedtuple trailed.
Types under test
| Kind | Notes |
|---|---|
| classic | normal class + __init__ |
| slotted | __slots__ = (a,b,c,d) |
| dataclass | @dataclass |
| dataclass_slots | @dataclass(slots=True) |
| namedtuple | collections.namedtuple |
| dict | plain {"a":...} |
| SimpleNamespace | convenience object |
Related links:
Lab topology
Script: lab-evidence/44-dataclass-vs-slots/results/run_lab.py.
Lead table — instantiation (p50 ops/s)
| Kind | ops/s | ns/op | vs classic |
|---|---|---|---|
| dict | 10 297 168 | 97 | 1.07× |
| dataclass | 10 163 726 | 98 | 1.05× |
| classic | 9 655 348 | 104 | 1.00× |
| dataclass(slots) | 8 310 537 | 120 | 0.86× |
| slotted | 7 607 139 | 132 | 0.79× |
| SimpleNamespace | 7 309 848 | 137 | 0.76× |
| namedtuple | 6 307 249 | 159 | 0.65× |
Lead table — attribute access (p50 ops/s)
| Kind | ops/s | vs classic |
|---|---|---|
| slotted | 20 909 681 | 1.01× |
| classic | 20 615 547 | 1.00× |
| dataclass(slots) | 20 601 969 | 1.00× |
| dataclass | 20 410 582 | 0.99× |
| dict | 17 971 517 | 0.87× |
| namedtuple | 16 159 996 | 0.78× |
| SimpleNamespace | 14 588 561 | 0.71× |
Access speed is not why you pick slots on 3.13 for this shape — memory is.
Related links:
Memory — 200k objects
| Kind | tracemalloc peak | RSS Δ | ~B/obj |
|---|---|---|---|
| dataclass(slots) | 25.9 MB | 20 744 KB | ~106 |
| slotted | 25.9 MB | 41 368 KB | ~212 |
| namedtuple | 29.0 MB | 23 312 KB | ~119 |
| dataclass | 33.6 MB | 31 680 KB | ~162 |
| classic | 33.6 MB | 83 004 KB | ~425 |
| dict | 48.8 MB | 57 672 KB | ~295 |
| SimpleNamespace | 56.5 MB | 68 372 KB | ~350 |
VmRSS deltas are noisy (allocator reuse); tracemalloc peaks and slotted vs dict contrast are the durable lesson.
Related links:
How to read these numbers
- Hot attribute loops: class / dataclass / slots are peers.
- Millions of small records: prefer
dataclass(slots=True)or slots for RAM. - Flexible JSON-shaped data: dict still fine (fastest inst here).
- namedtuple: immutable + lighter than classic, slower attr than slots.
Pitfalls we hit (or avoided)
- Claiming slots always instantiate faster — they were slower to construct here.
- Trusting only VmRSS — report tracemalloc too.
- Extrapolating to huge objects — four small fields only.
- Ignoring typed dataclass codegen — 3.13 is fast without slots for CPU.
- SimpleNamespace as “free object” — heavier and slower attr here.
Practical checklist
- Default new structured records:
@dataclass(addslots=Trueif counts are huge). - Keep dicts at JSON boundaries; convert once if needed.
- Don’t rewrite for attr µs — rewrite for RAM or API clarity.
- Re-bench on your Python minor version.
- Pair with orjson lab if dict↔JSON dominates.
Related links:
- json vs orjson vs msgpack localhost lab
- sha256 vs blake2 xxhash localhost lab
- atomic rename vs overwrite localhost lab
- asyncio vs threads IO concurrency lab
Methodology footnote
Instantiation builds throwaway objects in a tight loop. Attribute tests reuse one instance. RSS experiment holds a list of 200k instances, checksums .a / ["a"], then drops the list.
Versions pinned
- Python 3.13.5
- Fields: two ints + two short strings
If your heap is full of tiny records, slots are a memory feature with a small construct tax — not an attribute turbo button on this version.
When dict still wins
At JSON/API edges, dict is the native shape. This lab’s construct win for dict (~10.3M/s) means “don’t fear dict for short-lived request objects.” Convert to slotted dataclasses when you retain millions in memory (caches, graphs, feature rows). The orjson lab pairs naturally: decode to dict fast, then materialize structured objects only if lifetime justifies it.
slots construct tax
Both manual __slots__ and dataclass(slots=True) instantiated slower than classic/dataclass here (0.79–0.86×). You buy a smaller __dict__-less layout, not a faster __init__. If startup allocates a huge graph once, pay the tax once; if you churn objects every request, measure — plain dataclass may be the happier default on 3.13.
Ratio cheatsheet (vs classic)
Instantiate: dict 1.07× · dataclass 1.05× · slots-dc 0.86× · slotted 0.79× · namedtuple 0.65×
Attribute: slotted 1.01× · slots-dc 1.00× · dataclass 0.99× · dict 0.87× · namedtuple 0.78×
Use this when someone claims “slots are always faster” — say “faster for RAM; attribute peer; construct slightly slower here.”
Re-run after upgrading Python — dataclass codegen and specialty dictionaries change across minors.
Bottom line for API design
Expose dicts at boundaries; store slotted dataclasses inside long-lived services. That hybrid matches both the construct and RSS tables without pretending attribute access alone will save the quarterly cloud bill.
Verdict
On 3.13.5, attribute access for classic / dataclass / slots sat near ~20.5–20.9M/s while dict lagged ~13%. dataclass(slots) used ~106 B/obj versus dict ~295. Instantiate with dict/dataclass for speed; slot when RSS matters.
Evidence: lab-evidence/44-dataclass-vs-slots/results/. Affiliates: 0.
Lab evidence
What I found running this
Lab 1 Oct 2026 IST. Python 3.13.5. Inst ops/s: dict 10.3M; dataclass 10.2M; classic 9.7M; dataclass(slots) 8.3M; slotted 7.6M; namedtuple 6.3M. Attr: slotted 20.9M; classic/dataclass/slots ~20.4-20.6M; dict 18.0M; namedtuple 16.2M. RSS 200k objs: dataclass_slots ~106 B/obj; slotted ~212; namedtuple ~119; dataclass ~162; dict ~295; classic ~425 (tracemalloc peaks 25.9 vs 33.6 vs 48.8 MB). Affiliates: 0. Evidence: lab-evidence/44-dataclass-vs-slots/.
Related links
Plate 15
attrgetter vs getattr: Hot Loop Lab
Hands-on operator.attrgetter vs builtin getattr lab: real ops/s for extract and sort by attribute versus direct access, measured on Linux localhost only.
Observability & SRE · 30 Sept 2026
Plate 12
islice vs list Slice Windows: Localhost Lab
Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.
Observability & SRE · 1 Oct 2026
Plate 07
heapq.merge vs sorted(chain): Localhost Lab
Hands-on heapq.merge vs sorted(chain) multi-way merge: real records/s on pre-sorted lists, measured on Linux localhost today in this hands-on lab for SREs.
Observability & SRE · 1 Oct 2026