Skip to content

Dataclass Inheritance

Dataclasses support inheritance with automatic handling of parent and child fields. Understanding field ordering rules is important.

Mental Model

When a child dataclass inherits from a parent, Python concatenates the parent's fields before the child's fields to build __init__. This means a parent field with a default followed by a child field without one creates an illegal signature (positional after keyword). Think of it as stacking cards: the parent's cards go on the bottom, the child's on top, and the resulting deck must follow Python's argument-ordering rules.


Basic Inheritance

```python from dataclasses import dataclass

@dataclass class Animal: name: str age: int

@dataclass class Dog(Animal): breed: str

dog = Dog("Buddy", 3, "Golden Retriever") print(dog) # Dog(name='Buddy', age=3, breed='Golden Retriever') ```

Field Ordering in Inheritance

```python from dataclasses import dataclass, field

@dataclass class Base: x: int y: int = 10

Fields with defaults must come after fields without defaults

@dataclass class Derived(Base): z: int = 20 # Must have default since parent has default field

derived = Derived(1) print(derived) # Derived(x=1, y=10, z=20)

This would fail: fields without defaults after those with defaults

@dataclass

class BadDerived(Base):

z: int # Error: non-default field after default field

```

Overriding Parent Fields

```python from dataclasses import dataclass, field

@dataclass class Vehicle: wheels: int = 4 color: str = "white"

@dataclass class Car(Vehicle): # Override with different default wheels: int = 4 # Explicitly set color: str = "silver" # Different default doors: int = 4

car = Car() print(car) # Car(wheels=4, color='silver', doors=4) ```

Multiple Inheritance

```python from dataclasses import dataclass

@dataclass class TimestampMixin: created_at: str = "2024-01-01"

@dataclass class NameMixin: name: str

@dataclass class Document(NameMixin, TimestampMixin): content: str = ""

doc = Document(name="Report", content="Summary") print(doc) # Document(name='Report', created_at='2024-01-01', content='Summary') ```

Initialization Behavior

```python from dataclasses import dataclass

@dataclass class Parent: x: int

def __post_init__(self):
    print(f"Parent init: x={self.x}")

@dataclass class Child(Parent): y: int = 0

def __post_init__(self):
    super().__post_init__()  # Call parent
    print(f"Child init: y={self.y}")

child = Child(1, 2)

Output:

Parent init: x=1

Child init: y=2

```

Inheritance with Mutable Defaults

```python from dataclasses import dataclass, field from typing import List

@dataclass class BaseCollection: items: List[str] = field(default_factory=list)

@dataclass class ExtendedCollection(BaseCollection): metadata: dict = field(default_factory=dict)

col1 = ExtendedCollection() col2 = ExtendedCollection()

col1.items.append("item1") col1.metadata['type'] = 'test'

print(col1) # ExtendedCollection(items=['item1'], metadata={'type': 'test'}) print(col2) # ExtendedCollection(items=[], metadata={}) # Independent ```

Best Practices

  • Keep parent dataclasses simple
  • Place required fields in parent, optional in child
  • Call super().__post_init__() when overriding
  • Consider composition over inheritance for complex cases

Dataclass inheritance is convenient but brittle

Keep hierarchies shallow (one level of inheritance is ideal). Beyond two levels, field ordering constraints compound, defaults become hard to manage, and __post_init__ chaining grows fragile. If you find yourself fighting the framework, flatten the hierarchy or switch to composition.

When NOT to use inheritance

Dataclass inheritance works well for shallow, "is-a" hierarchies (e.g., Dog is an Animal). Prefer composition when:

  • Behavior differs significantly between parent and child — you end up overriding most methods, defeating the purpose of reuse.
  • The hierarchy grows deeper than two levels — field ordering constraints compound and defaults become hard to manage.
  • You need to mix capabilities from multiple unrelated sources — multiple inheritance with dataclasses can produce surprising MRO and field-order issues.

```python

Composition instead of deep inheritance

@dataclass class Engine: horsepower: int fuel_type: str

@dataclass class Car: make: str engine: Engine # has-a, not is-a ```


Exercises

Exercise 1. Create a parent dataclass Vehicle with fields make (str) and year (int). Create a child dataclass Car that adds doors (int, default 4) and electric (bool, default False). Create instances of both and print them. Show that Car inherits fields from Vehicle.

Solution to Exercise 1
from dataclasses import dataclass

@dataclass
class Vehicle:
    make: str
    year: int

@dataclass
class Car(Vehicle):
    doors: int = 4
    electric: bool = False

v = Vehicle("Toyota", 2023)
c = Car("Tesla", 2024, doors=4, electric=True)

print(v)  # Vehicle(make='Toyota', year=2023)
print(c)  # Car(make='Tesla', year=2024, doors=4, electric=True)
print(c.make)  # Tesla — inherited from Vehicle

Exercise 2. Define a base dataclass Shape with a color field (str, default "black"). Create child dataclasses Circle (adds radius) and Rectangle (adds width and height). Add area() methods to each child. Be careful with field ordering: required fields must come before fields with defaults. Show a working solution.

Solution to Exercise 2
from dataclasses import dataclass
import math

@dataclass
class Shape:
    color: str = "black"

@dataclass
class Circle(Shape):
    radius: float = 1.0  # Must have default (parent has default)

    def area(self):
        return math.pi * self.radius ** 2

@dataclass
class Rectangle(Shape):
    width: float = 1.0
    height: float = 1.0

    def area(self):
        return self.width * self.height

c = Circle(color="red", radius=5)
r = Rectangle(color="blue", width=3, height=4)

print(f"{c} -> area={c.area():.2f}")
# Circle(color='red', radius=5) -> area=78.54
print(f"{r} -> area={r.area():.2f}")
# Rectangle(color='blue', width=3, height=4) -> area=12.00

Exercise 3. Create a parent dataclass Employee with name (str) and department (str). Create a child Manager that adds team_size (int) and overrides __post_init__ to validate that team_size is positive (calling super().__post_init__() if the parent has one). Demonstrate that creating a Manager with team_size=0 raises a ValueError.

Solution to Exercise 3
from dataclasses import dataclass

@dataclass
class Employee:
    name: str
    department: str

    def __post_init__(self):
        if not self.name:
            raise ValueError("Name cannot be empty")

@dataclass
class Manager(Employee):
    team_size: int = 1

    def __post_init__(self):
        super().__post_init__()
        if self.team_size <= 0:
            raise ValueError(f"team_size must be positive, got {self.team_size}")

m = Manager("Alice", "Engineering", team_size=5)
print(m)  # Manager(name='Alice', department='Engineering', team_size=5)

try:
    bad = Manager("Bob", "Sales", team_size=0)
except ValueError as e:
    print(f"Error: {e}")
    # Error: team_size must be positive, got 0