Hierarchies of models (base entity, timestamps, subtypes) are common and have sharp edges.
When a dataclass inherits from another dataclass, the decorator collects fields from base classes in reverse MRO order (base first), then adds the subclass's own fields. A redefined field keeps its original position but takes the new type and default.
The famous error: the base has a field with a default and the subclass adds a field without one:
@dataclass
class Base:
id: int
active: bool = True
@dataclass
class User(Base):
name: str # TypeError: non-default argument 'name' follows default argumentThe generated __init__(id, active=True, name) would be invalid. Fixes:
- make the new fields keyword-only (
kw_only=Trueon the subclass, orfield(kw_only=True)), the cleanest option since 3.10, - give the new field a default too,
- or restructure so defaults live in the most-derived class.
Other rules:
__post_init__is not chained automatically: callsuper().__post_init__()in the subclass if the base defines one.- Decorator options aren't inherited per se: each class gets the options of its own
@dataclass(...). A frozen dataclass can't inherit from a non-frozen one, or vice versa. eqcompares only instances of the exact same class, so aUsernever equals aBasewith the same fields.
Example
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class Entity:
id: int
created: datetime = field(default_factory=lambda: datetime(2026, 1, 1))
def __post_init__(self):
if self.id <= 0:
raise ValueError("id must be positive")
@dataclass(kw_only=True)
class User(Entity):
name: str # required, but keyword-only: no ordering error
email: str = ""
def __post_init__(self):
super().__post_init__() # not called automatically
self.email = self.email.lower()
u = User(7, name="Ada", email="ADA@X.IO")
print(u) # User(id=7, created=datetime.datetime(2026, 1, 1, 0, 0), name='Ada', email='ada@x.io')
print(u == Entity(7)) # False: different classesRemember
- Fields are collected base-first; subclass fields come after
- A field without a default after inherited defaults fails; kw_only fixes it
- Call super().__post_init__() explicitly
Watch out for
- Forgetting super().__post_init__(), silently skipping the base class's validation.