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 defaultThe 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 designBetter approach—put required fields in base, defaults in children:
@dataclass
class Base:
id: int
name: str
@dataclass
class User(Base):
email: str
active: bool = TrueOverriding 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) # 100Overriding 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 ** 2Using 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, 7Mixin 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 timestampAbstract 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()) # 1Frozen 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)) # 2Factory 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
- Avoid deep hierarchies: Keep inheritance shallow
- Required fields in base: Put defaults in children
- Call super().post_init(): Don't forget parent initialization
- Prefer composition: Use nested dataclasses over inheritance
- 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__withsuper()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: