Dataclass inheritance

dataclasses · Advanced · 11 min

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 argument

The generated __init__(id, active=True, name) would be invalid. Fixes:

  • make the new fields keyword-only (kw_only=True on the subclass, or field(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: call super().__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.
  • eq compares only instances of the exact same class, so a User never equals a Base with 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 classes

Remember

  • 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.

Check yourself

  1. Base has a field with a default; the subclass adds a field without a default. What's the cleanest fix (3.10+)?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/dataclasses.html#inheritance