Class inheritance in Python 3.7 dataclasses
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Python 3.7 introduced the dataclasses module, which automatically generates boilerplate methods like __init__, __repr__, and __eq__ for classes that primarily hold data. Dataclasses work with inheritance, but there is one critical ordering rule that trips up nearly every developer the first time they encounter it. Understanding this rule and its solutions will save you from a confusing TypeError at class definition time.
Dataclass Basics
Before looking at inheritance, let us review what a basic dataclass looks like and why it exists. The @dataclass decorator inspects the class body for field annotations and generates an __init__ method whose parameters match those fields in order:
This single decorator replaces about a dozen lines of handwritten __init__, __repr__, and __eq__ code. You create instances just like any other class:
Fields can have default values:
Just like function parameters, fields with defaults must come after fields without defaults. This rule matters a great deal when inheritance enters the picture.
Basic Dataclass Inheritance
A child dataclass can inherit from a parent dataclass. The generated __init__ combines the parent's fields followed by the child's fields:
The __init__ signature becomes (self, make, model, num_doors, is_electric). Parent fields appear first, child fields appear second. This works perfectly as long as you follow the default-value ordering rule.
The Field Ordering Problem
Here is where most developers hit a wall. Suppose the parent class has a field with a default value, and the child class adds a field without one:
This raises a TypeError at class definition time:
The reason is that the combined __init__ would have the signature (self, make, model, year=2024, num_doors). In Python, you cannot have a parameter without a default after a parameter with a default. The dataclass decorator enforces this rule when it generates __init__.
Solutions to the Ordering Problem
Solution 1 -- Give All Child Fields Defaults
The most straightforward fix is to ensure every child field has a default value:
This works because the combined signature (self, make, model, year=2024, num_doors=4, is_electric=False) is valid. The downside is that you might want num_doors to be required, which this approach does not enforce.
Solution 2 -- Remove Defaults from Parent Fields
If you control the parent class, you can remove the defaults so all fields are required:
This is clean but forces every caller to provide year explicitly.
Solution 3 -- Use field() with default_factory
For mutable defaults (like lists or dicts), always use field(default_factory=...):
Using a bare [] as a default would raise a ValueError because mutable defaults are shared across instances. The field(default_factory=list) creates a new list for each instance.
Solution 4 -- Reorder with a Non-Dataclass Base (Python 3.10+)
In Python 3.10 and later, you can use kw_only=True to sidestep the ordering issue entirely:
With kw_only=True, all fields become keyword-only arguments, so ordering no longer matters. This is the cleanest solution if you do not need to support Python 3.7-3.9.
Using post_init
The __post_init__ method runs immediately after the generated __init__ completes. It is the right place for validation or computed fields:
Notice the explicit super().__post_init__() call. The dataclass decorator does not chain __post_init__ calls automatically, so you must call the parent's version yourself if the parent defines one.
Calling super().init
Because the @dataclass decorator generates __init__, you generally should not write your own __init__. If you do need custom initialization logic, put it in __post_init__ instead. If for some reason you override __init__ in a child, you need to call super().__init__() manually and pass all parent fields, which defeats much of the purpose of using dataclasses.
Common Pitfalls
- Adding a child field without a default when the parent has fields with defaults. This is the single most common dataclass inheritance mistake.
- Using mutable objects (lists, dicts, sets) as default values instead of using
field(default_factory=...). - Forgetting to call
super().__post_init__()in the child class when the parent defines its own__post_init__. - Assuming
field(init=False)fields are automatically computed. You still need to set them in__post_init__. - Overriding
__init__directly instead of using__post_init__, which bypasses the benefits of the dataclass decorator.
Summary
- Dataclass inheritance combines parent fields (first) with child fields (second) in the generated
__init__. - The
TypeErrorabout non-default arguments following default arguments is caused by a child adding required fields after a parent defines optional ones. - Fix the ordering issue by giving child fields defaults, removing parent defaults, using
field(default_factory=...), or usingkw_only=Truein Python 3.10+. - Use
__post_init__for validation and computed fields, and remember to callsuper().__post_init__()explicitly. - Prefer
field(default_factory=list)over bare[]for mutable default values to avoid shared-state bugs.

