Inheritance, Composition, dataclasses & Value Objects
Object-oriented design in Python balances code reuse against architectural coupling. While inheritance establishes βIs-Aβ relationships, excessive inheritance creates fragile base class problems. Modern Python engineering favors Composition (βHas-Aβ) and immutable Value Objects created via dataclasses (PEP 557).
This chapter details Inheritance vs Composition trade-offs, Value Object immutability, dataclass code generation mechanics, and __post_init__ validation hooks.
1. Inheritance vs. Composition Architectural Invariants
- Inheritance (βIs-Aβ): Subclasses inherit behavior and state from a base class. Use inheritance strictly when the child class is a specialized subtype of the parent class, fulfilling the Liskov Substitution Principle (LSP).
- Composition (βHas-Aβ): A class delegates responsibility by referencing component objects. Composition decouples components, making software easier to test, extend, and refactor.
Inheritance vs Composition Architecture:
Inheritance (Tight Coupling):
[ BaseWorker ]
|
v
[ ProcessingWorker ] (Changes in BaseWorker risk breaking ProcessingWorker!)
Composition (Decoupled Wiring):
[ ProcessingWorker ] ββ> Uses ββ> [ StrategyInterface ]
βββ [ KafkaPublisher ]
βββ [ PostgresLogger ]2. Standard Data Containers: dataclasses (PEP 557)
Prior to Python 3.7, creating data container classes required writing repetitive boilerplate (__init__, __repr__, __eq__, __hash__).
The @dataclass decorator inspects class type annotations and generates C-optimized dunder methods automatically at class definition time:
from dataclasses import dataclass, field
from datetime import datetime
@dataclass(frozen=True, slots=True)
class UserProfile:
user_id: int
email: str
created_at: datetime = field(default_factory=datetime.now)Generated Methods:
__init__(): Assigns typed fields.__repr__(): Formats explicit string representation (UserProfile(user_id=1, ...)).__eq__(): Evaluates value equality across all fields.__hash__(): Generates hash code (enabled whenfrozen=True).slots=True(Python 3.10+): Generates__slots__automatically for memory optimization.
3. Immutability & Value Objects (frozen=True)
In Domain-Driven Design (DDD), a Value Object is defined entirely by its attributes (e.g. Money(amount=100, currency="USD")) rather than an identity key. Value Objects must be immutable.
Setting frozen=True on a @dataclass enforces immutability by intercepting attribute writes:
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str
m = Money(Decimal("50.00"), "USD")
m.amount = Decimal("100.00") # RAISES FrozenInstanceError!4. Custom Initialization & Validation (__post_init__)
When a @dataclass generates __init__(), it calls __post_init__() immediately after assigning fields. Use __post_init__ for domain validation and derived field computation:
@dataclass
class Order:
items: list[str]
total_price: Decimal
def __post_init__(self):
# Validation Hook
if self.total_price < 0:
raise ValueError("Total price cannot be negative")
if not self.items:
raise ValueError("Order must contain at least one item")