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 when frozen=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")
Display Options
Appearance
Text Size
100%