HOWTO · Python
Data Class Inheritance in Python
Learn how dataclass inheritance collects and overrides fields, why field order can raise TypeError, and how to fix and inspect the generated initializer.
On this page
Python dataclasses use annotated class attributes as fields and generate methods such as __init__ and __repr__. When one dataclass inherits from another, its generated initializer includes the inherited fields as well as fields declared by the subclass. Understanding the resulting field order helps you choose constructor arguments and diagnose a TypeError when you extend a class.
How dataclass inheritance combines fields
Decorate both the base and derived class with @dataclass when you want each class’s declared fields to participate in the generated dataclass behavior. The base fields come first, followed by new subclass fields. A subclass can also override an inherited field: the override keeps the field’s position and changes its definition, such as its default value. The following example shows how the generated initializer includes inherited fields before the subclass field.
Run this example:
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
@dataclass
class Student(Person):
school: str
student = Student("Ada Lovelace", 17, "North School")
print(student.name)
print(f"{student.name}: {student.age}, school={student.school}")
Ada Lovelace
Ada Lovelace: 17, school=North School
Dataclasses collect fields across the dataclass bases in reverse method resolution order, then add or replace fields from the current class. An override does not move the field to the end. This ordering is important because it determines the generated initializer’s parameter order and whether a required field follows a field with a default.
Use inherited fields across multiple levels
Ordinary classes can also form a multi-level hierarchy, but that alone does not make them dataclasses or generate dataclass fields. In an ordinary hierarchy, super() calls the next implementation according to the class’s method resolution order (MRO). Each participating method should accept compatible arguments and call super() when it is meant to cooperate with the rest of the hierarchy. This is useful for constructors and other methods; it is not a compiler walking back through parent methods.
Here, each show method adds one to the value before handing it to the next implementation. The trace makes the MRO sequence visible:
class Root:
def show(self, value):
print("Root:", value)
class Middle(Root):
def show(self, value):
print("Middle:", value)
super().show(value + 1)
class Leaf(Middle):
def show(self, value):
print("Leaf:", value)
super().show(value + 1)
Leaf().show(10)
Leaf: 10
Middle: 11
Root: 12
This example uses regular classes to isolate cooperative method calls from dataclass field collection. Dataclass inheritance follows the same Python MRO for class relationships, while dataclass field collection and initializer generation add their own rules.
Why dataclass field order raises a TypeError
A generated initializer cannot put a required positional parameter after one with a default. This restriction applies across inheritance just as it does within one dataclass. For example, if a base class declares required name and then active: bool = False, adding required school in a dataclass subclass would produce the order name, active, school. The generated signature would put the required school after a defaulted parameter, so class creation raises TypeError: non-default argument 'school' follows default argument.
Overriding active with another default does not change its original position. It therefore does not move the defaulted field after school. When this error appears, inspect the complete inherited field sequence rather than only the subclass body: a default in any dataclass base can be the cause. Also check that the class is actually decorated with @dataclass; ordinary annotated attributes do not by themselves participate in dataclass field processing.
Choose a safe fix and inspect the generated initializer
Choose the fix that matches the API you want to expose. If a subclass field is required but should not be a positional argument, make it keyword-only with field(kw_only=True). This preserves the required value while removing it from the positional parameter sequence. Other valid designs include giving the new field a meaningful default or making earlier fields keyword-only; avoid rearranging unrelated base classes just to manipulate field order, since that makes the hierarchy difficult to understand and maintain.
Use inspect.signature to check the public initializer after changing the fields. In this example, school remains required, but callers must supply it by name:
from dataclasses import dataclass, field
from inspect import signature
@dataclass
class Person:
name: str
age: int
active: bool = False
@dataclass
class Student(Person):
school: str = field(kw_only=True)
active: bool = True
print(signature(Student))
print(Student("Mina", 20, school="North School"))
(name: str, age: int, active: bool = True, *, school: str) -> None
Student(name='Mina', age=20, active=True, school='North School')
The signature confirms that active retains its inherited position and receives the subclass default, while school is required after * and must be passed by keyword. If the signature differs from the API you intend, review the inherited annotations and defaults, then adjust the dataclass fields rather than relying on a complicated base-class ordering workaround.