In Python, a frozen dataclass costs four times as much to create as a plain one
I built the same four-field record eight ways in Python 3.14 and timed creating and reading it. A plain class and a @dataclass came out identical. slots=True made them 38% smaller and about 13% faster to create. frozen=True on top made creation 4.5 times slower, and namedtuple reads were nearly as slow as a dict's.
The test
Four int fields, a to d. Creation is timeit on a call with literal arguments (2 million calls, best of 7). Reading is o.a + o.d (or the dict/tuple equivalent) minus the cost of 1 + 4. Memory is the bytes tracemalloc sees for 100,000 instances, minus the list holding them. CPython 3.14.8 in a container pinned to one CPU, three runs one after another; the spread was under 1 ns everywhere except a few ns on the frozen case.
| record | create (ns) | read two fields (ns) | bytes each |
|---|---|---|---|
| plain class | 40.5 | 5.1 | 104 |
| @dataclass | 40.4 | 5.1 | 104 |
| class with __slots__ | 35.0 | 4.8 | 64 |
| @dataclass(slots=True) | 34.9 | 4.8 | 64 |
| @dataclass(frozen=True, slots=True) | 159 | 4.8 | 64 |
| namedtuple | 90 | 14.2 | 88 |
| dict | 41.8 | 15.0 | 184 |
| tuple | ~13 | 6.3 | 80 |
The tuple's creation time comes from a separate run with the values in variables. With constants in the literal, Python builds the tuple once at compile time and the loop measures nothing (2 ns).
What it shows
Dataclass is free. The generated __init__ is the same code as the one you would write, so the two rows match to within noise.
Slots pay in memory first. 64 bytes against 104, because the instance has no per-object dict. Creation is about 5 ns faster and reads about 0.3 ns faster, small next to the memory saving at a million objects (40 MB).
Frozen is the expensive one. A frozen dataclass sets its fields through object.__setattr__ in the generated __init__, once per field, and that is where 120 ns goes. Reads are as fast as any slots class, so it is a cost on the creation path only.
Namedtuple creation and reads both lag. Creating one goes through tuple.__new__ from Python code at 90 ns, more than twice a class. A field read takes 14 ns, about the cost of a dict lookup and nearly three times an attribute on a slots class. A plain tuple indexed by number reads in 6.3 ns, so the names cost 8 ns per read.
So what
For many small records, use @dataclass(slots=True): the smallest and fastest to create, with the same read speed as anything else. Add frozen=True only when you need hashing or immutability, and not on a path that creates millions of them per second. Prefer a class to a namedtuple when you read fields in a hot loop. A dict is the worst on memory (184 bytes) and reads, though not on creation.
Method: one machine, an 80-line script, CPython 3.14.8 (default build, no JIT). Four int fields, small ints that Python caches; records with larger fields would add the same amount to every row. Frozen dataclasses with different field counts weren't tested.