Back to Blog
Python

Python Dataclass Inheritance: Syntax and Common Pitfalls

Learn how python dataclass inheritance works: field collection, default value ordering, kw_only, overrides, and post_init behavior.

dataclassesinheritancepythonobject-oriented-programmingtype-hints
Illustration of a parent dataclass node connected to a child dataclass node with field boxes flowing between them

When you apply @dataclass to a subclass, Python does not treat the subclass as a fresh class with its own field set. It collects fields from the parent and child into a single ordered mapping, then generates __init__, __repr__, __eq__, and related methods from that combined list. Understanding how this collection works is the key to using python dataclass inheritance without running into ordering errors or silent behavior changes.

How Fields Are Collected Across Class Boundaries

A subclass inherits every field declared in its dataclass parents. The field order is determined by declaration order in the base class first, then by declaration order in the subclass. New fields in the child are appended after all inherited fields.

from dataclasses import dataclass @dataclass class Base: name: str priority: int = 1 @dataclass class Task(Base): completed: bool = False

The Task class has three fields: name, priority, and completed. Its generated __init__ signature is Task(name: str, priority: int = 1, completed: bool = False). You do not need to re-declare inherited fields unless you want to change their type, default, or metadata.

This collection behavior also means that the __eq__ and __repr__ methods compare and display all inherited fields. Two Task instances are equal only when name, priority, and completed all match.

The Default Value Ordering Constraint

The most common failure in python dataclass inheritance comes from Python's rule that a parameter with a default value cannot precede a parameter without one. Because the child's fields are appended after the parent's fields, a child field without a default will always sit after a parent field that has a default.

from dataclasses import dataclass @dataclass class Base: name: str = "untitled" @dataclass class Child(Base): priority: int # raises TypeError at class definition

This raises TypeError: non-default argument 'priority' follows default argument when the module is imported. The error is raised at class creation time, not at instantiation, so it can be confusing when the failing line is not obviously related to the actual field ordering.

There are three fixes:

  • give priority a default value,
  • remove the default from name,
  • mark name as keyword-only.

The third option is usually the right choice when the parent field is genuinely optional and the child adds required fields.

Using kw_only to Break the Ordering Constraint

Python 3.10 added the kw_only parameter to @dataclass. When set to True, all fields in that class are marked as keyword-only. Keyword-only fields do not participate in positional ordering constraints, which makes them useful when a parent dataclass has defaulted fields and a child must add required fields.

from dataclasses import dataclass @dataclass(kw_only=True) class Base: name: str = "untitled" @dataclass class Child(Base): priority: int

Here, name is keyword-only because it was declared in a class with kw_only=True. The child's priority field is positional. The generated __init__ signature is Child(priority: int, *, name: str = "untitled"), which is valid because the positional parameter comes first and the keyword-only parameter has a default.

You can also set kw_only=True on the child class to make all of the child's new fields keyword-only as well:

from dataclasses import dataclass @dataclass class Base: name: str @dataclass(kw_only=True) class Child(Base): priority: int = 1

The name field remains positional, and priority becomes keyword-only. The generated signature is Child(name: str, *, priority: int = 1).

Overriding Fields and Methods in Subclasses

You can re-declare a field in a subclass to change its default value, type annotation, or metadata. The re-declared field replaces the parent's field in the ordered mapping, but it keeps its original position in the field order.

from dataclasses import dataclass, field @dataclass class Base: name: str tags: list[str] = field(default_factory=list) @dataclass class Child(Base): name: str = "unnamed" tags: list[str] = field(default_factory=lambda: ["new"])

In this example, name and tags keep their positions from the base class, but their defaults are replaced. The generated __init__ is Child(name: str = "unnamed", tags: list[str] = <factory>).

When both parent and child define __post_init__, the child's method does not automatically call the parent's. You must call super().__post_init__() explicitly:

from dataclasses import dataclass @dataclass class Base: name: str def __post_init__(self): self.name = self.name.strip() @dataclass class Child(Base): priority: int def __post_init__(self): super().__post_init__() if self.priority < 0: raise ValueError("priority must be non-negative")

If the child's __post_init__ does not call super().__post_init__(), the parent's validation and normalization logic is silently skipped. This is a common source of subtle bugs in inheritance hierarchies.

Frozen and Slotted Dataclasses in Inheritance

A frozen dataclass inherits its frozen behavior. When the parent uses frozen=True, the child is also frozen, even if the child does not explicitly set frozen=True. The child's __init__ sets all fields before the frozen flag is activated, so initialization works normally. After construction, any attribute assignment raises FrozenInstanceError.

from dataclasses import dataclass @dataclass(frozen=True) class Base: name: str @dataclass class Child(Base): priority: int

Child instances cannot have their attributes modified after creation. If you need a mutable subclass of a frozen parent, you must override the field with a mutable design or avoid freezing the parent.

The slots=True option, available since Python 3.10, has a different inheritance behavior. If the parent uses slots=True and the child adds new fields without also setting slots=True, the child gains a __dict__ for its new fields. The parent's fields still live in slots, but the child's new fields do not. To keep the entire hierarchy slot-based, set slots=True on every class in the hierarchy.

Practical Pitfalls and Their Fixes

The most common errors in python dataclass inheritance come from three sources: field ordering, forgotten super().__post_init__() calls, and re-declared fields with incompatible types.

Field ordering errors are always raised at class definition time. When you see TypeError: non-default argument follows default argument on a dataclass line, inspect the parent's field defaults first. The fix is usually kw_only=True on the parent or a default on the child field.

Forgotten super().__post_init__() calls do not raise errors. They silently skip parent initialization logic. If a parent dataclass defines __post_init__ and a child overrides it, the child must call super().__post_init__() to preserve parent behavior.

Re-declared fields with incompatible types do not raise errors either. Python's type checker will flag them, but at runtime the child's type annotation simply replaces the parent's. If you re-declare a field, make sure the new type is compatible with the parent's usage, especially in methods that the parent defines and the child inherits.

When a parent dataclass uses InitVar pseudo-fields, those are also inherited. The child's __init__ will accept the same InitVar parameters, and the child's __post_init__ receives them as arguments. If the child overrides __post_init__, it must accept the same InitVar parameters as the parent, or call super().__post_init__() with the appropriate values.

python dataclass inheritance: Practical Usage and Code Examp | RYUSLOG DEV