Dataclasses support inheritance, but there are patterns and pitfalls to understand.

Basic Inheritance

from dataclasses import dataclass
 
@dataclass
class Animal:
    name: str
    age: int
 
@dataclass
class Dog(Animal):
    breed: str
 
# Child includes parent fields
dog = Dog(name="Rex", age=5, breed="Labrador")
print(dog)  # Dog(name='Rex', age=5, breed='Labrador')

The Default Value Problem

from dataclasses import dataclass
 
@dataclass
class Base:
    required: str
    optional: str = "default"
 
# This fails!
@dataclass
class Child(Base):
    another_required: str  # Error: non-default follows default

The fix—child fields with defaults come after parent defaults:

from dataclasses import dataclass, field
 
@dataclass
class Base:
    required: str
    optional: str = "default"
 
@dataclass
class Child(Base):
    another_required: str = field(default="")  # Give it a default
    # OR reorder in your design

Better approach—put required fields in base, defaults in children:

@dataclass
class Base:
    id: int
    name: str
 
@dataclass
class User(Base):
    email: str
    active: bool = True

Overriding Fields

from dataclasses import dataclass
 
@dataclass
class Base:
    value: int = 0
 
@dataclass
class Child(Base):
    value: int = 100  # Override default
 
c = Child()
print(c.value)  # 100

Overriding Methods

from dataclasses import dataclass
 
@dataclass
class Shape:
    def area(self) -> float:
        raise NotImplementedError
 
@dataclass
class Rectangle(Shape):
    width: float
    height: float
    
    def area(self) -> float:
        return self.width * self.height
 
@dataclass
class Circle(Shape):
    radius: float
    
    def area(self) -> float:
        import math
        return math.pi * self.radius ** 2

Using post_init with Inheritance

from dataclasses import dataclass
 
@dataclass
class Base:
    x: int
    
    def __post_init__(self):
        print("Base post_init")
        self.x_squared = self.x ** 2
 
@dataclass
class Child(Base):
    y: int
    
    def __post_init__(self):
        super().__post_init__()  # Call parent
        print("Child post_init")
        self.sum = self.x + self.y
 
c = Child(x=3, y=4)
# Base post_init
# Child post_init
print(c.x_squared, c.sum)  # 9, 7

Mixin Classes

from dataclasses import dataclass
from datetime import datetime
 
class TimestampMixin:
    created_at: datetime = None
    
    def __post_init__(self):
        if self.created_at is None:
            object.__setattr__(self, 'created_at', datetime.now())
 
@dataclass
class User(TimestampMixin):
    name: str
    email: str
 
user = User(name="Alice", email="alice@example.com")
print(user.created_at)  # Current timestamp

Abstract Base Classes

from dataclasses import dataclass
from abc import ABC, abstractmethod
 
@dataclass
class Serializable(ABC):
    @abstractmethod
    def to_dict(self) -> dict:
        pass
 
@dataclass
class User(Serializable):
    name: str
    email: str
    
    def to_dict(self) -> dict:
        return {"name": self.name, "email": self.email}
 
# Can't instantiate Serializable directly
user = User(name="Alice", email="alice@example.com")
print(user.to_dict())

Composition Over Inheritance

Often better than inheritance:

from dataclasses import dataclass, field
from typing import List
 
@dataclass
class Address:
    street: str
    city: str
    country: str
 
@dataclass
class ContactInfo:
    email: str
    phone: str
 
@dataclass
class Person:
    name: str
    address: Address
    contact: ContactInfo
 
# Usage
person = Person(
    name="Alice",
    address=Address("123 Main St", "NYC", "USA"),
    contact=ContactInfo("alice@example.com", "555-0100")
)

Generic Dataclasses

from dataclasses import dataclass
from typing import TypeVar, Generic, List
 
T = TypeVar('T')
 
@dataclass
class Container(Generic[T]):
    items: List[T]
    
    def first(self) -> T:
        return self.items[0] if self.items else None
 
@dataclass
class NumberContainer(Container[int]):
    def sum(self) -> int:
        return sum(self.items)
 
nc = NumberContainer(items=[1, 2, 3, 4, 5])
print(nc.sum())  # 15
print(nc.first())  # 1

Frozen Inheritance

from dataclasses import dataclass
 
@dataclass(frozen=True)
class ImmutableBase:
    x: int
 
@dataclass(frozen=True)
class ImmutableChild(ImmutableBase):
    y: int
 
# Both are immutable
child = ImmutableChild(x=1, y=2)
# child.x = 10  # FrozenInstanceError!

Note: A frozen child can inherit from a non-frozen parent, but mixing can be confusing.

Field Inheritance with Metadata

from dataclasses import dataclass, field
from typing import ClassVar
 
@dataclass
class Base:
    # Class variable (not instance field)
    _registry: ClassVar[list] = []
    
    id: int
    
    def __post_init__(self):
        self._registry.append(self)
 
@dataclass
class User(Base):
    name: str
 
@dataclass
class Product(Base):
    title: str
 
# All instances tracked
u = User(id=1, name="Alice")
p = Product(id=2, title="Widget")
print(len(Base._registry))  # 2

Factory Pattern

from dataclasses import dataclass
from typing import Type, TypeVar
 
T = TypeVar('T', bound='Animal')
 
@dataclass
class Animal:
    name: str
    
    @classmethod
    def create(cls: Type[T], name: str) -> T:
        return cls(name=name)
 
@dataclass
class Dog(Animal):
    breed: str = "Unknown"
 
@dataclass
class Cat(Animal):
    indoor: bool = True
 
# Factory creates correct type
dog = Dog.create("Rex")  # Dog instance
dog.breed = "Labrador"

Best Practices

  1. Avoid deep hierarchies: Keep inheritance shallow
  2. Required fields in base: Put defaults in children
  3. Call super().post_init(): Don't forget parent initialization
  4. Prefer composition: Use nested dataclasses over inheritance
  5. Keep frozen consistent: Don't mix frozen and non-frozen

Summary

Dataclass inheritance patterns:

  • Child classes inherit all parent fields
  • Watch field ordering (defaults must come last)
  • Override __post_init__ with super() calls
  • Use composition for complex relationships
  • Abstract base classes for interfaces
  • Generics for type-safe containers

Inheritance works, but composition is often cleaner.

React to this post: