Python Composition & Dataclasses

🐍 Python 3.12+ 🟒 Chapter 34 of 65 πŸ“‚ Phase 7: Object-Oriented Programming πŸ“… 2026 Edition
πŸ“Œ Covered in this chapter: Composition ("Has-A") vs Inheritance ("Is-A") Β· Decoupling Β· @dataclass (PEP 557) Β· field(default_factory) Β· Frozen Immutability
Master clean decoupled software architecture in Python: favoring composition over inheritance ("Has-A" vs "Is-A"), and dramatically reducing boilerplate code using modern Python dataclasses (PEP 557).
1Composition Over Inheritance ("Has-A" Relationship)

A famous principle in software architecture states: "Favor Object Composition over Class Inheritance".

  • Inheritance ("Is-A"): Tight coupling. A Dog is an Animal. Changes to the parent class risk breaking all child classes across a large codebase.
  • Composition ("Has-A"): Loose coupling. A Car has an Engine. Complex systems are assembled by combining independent, reusable building block components.
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Composite Class: Car β”‚ β”‚ β”œβ”€β”€ Has-A: Engine Object (v8_engine) β”‚ β”‚ └── Has-A: List of Tire Objects (4 Michelin tires) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
πŸ’» Example 1: Building Modular Systems with Composition
# Independent Component Classes:
class Engine:
    def __init__(self, horsepower, fuel_type):
        self.horsepower = horsepower
        self.fuel_type = fuel_type

    def ignite(self):
        return f"πŸ”₯ Engine ({self.horsepower} HP, {self.fuel_type}) ignited and purring!"

class GPSNavigator:
    def route_to(self, destination):
        return f"πŸ—ΊοΈ Calculating shortest route to '{destination}'..."

# Composite Class assembling components:
class Automobile:
    def __init__(self, model, engine_hp, fuel_type):
        self.model = model
        # COMPOSITION: Automobile "has an" Engine and "has a" GPS:
        self.engine = Engine(engine_hp, fuel_type)
        self.navigator = GPSNavigator()

    def start_trip(self, destination):
        print(f"--- Starting Trip in {self.model} ---")
        print(self.engine.ignite())
        print(self.navigator.route_to(destination))

my_car = Automobile("Porsche 911", 450, "Petrol")
my_car.start_trip("Hyderabad Cyber Towers")
πŸ” Flexibility:

If you want an electric car, you can simply swap self.engine = ElectricMotor() without altering the vehicle's navigation, chassis, or seating logic.

2Modern Dataclasses: Eliminating Boilerplate (PEP 557)

Introduced in Python 3.7 (PEP 557), @dataclass automatically writes boilerplate code for you, generating:

  • __init__() with type annotations.
  • __repr__() for clean printing.
  • __eq__() for value-based comparison.
πŸ’» Example 2: Defining Dataclasses with PEP 557
from dataclasses import dataclass, field
from typing import List

# Automatically generates __init__, __repr__, and __eq__ in 5 lines:
@dataclass
class StudentProfile:
    student_id: int
    name: str
    branch: str
    gpa: float = 0.0
    skills: List[str] = field(default_factory=list) # Safe mutable list!

# Instantiating dataclasses:
s1 = StudentProfile(101, "Balaji", "Computer Science", 9.4, ["Python", "FastAPI"])
s2 = StudentProfile(101, "Balaji", "Computer Science", 9.4, ["Python", "FastAPI"])

print("--- Dataclass Generated Representation ---")
print(s1)

print("\n--- Automatic Value Equality (__eq__) ---")
print("s1 == s2?", s1 == s2) # True! Automatically compares all fields!
πŸ” field(default_factory=list):

Never use skills: List[str] = [] in a dataclass. Always use field(default_factory=list) to ensure a fresh new list is created for every instance.

3Immutable Value Objects: Frozen Dataclasses

Adding frozen=True creates an immutable dataclass that acts like a write-protected tuple. It can also be hashed and stored in sets or used as dictionary keys:

πŸ’» Example 3: Immutable Read-Only Dataclasses with frozen=True
from dataclasses import dataclass

@dataclass(frozen=True)
class GeoCoordinates:
    latitude: float
    longitude: float

location = GeoCoordinates(17.3850, 78.4867)
print("Geo Coordinates:", location)

# Attempting mutation raises FrozenInstanceError:
try:
    location.latitude = 18.0000
except Exception as err:
    print("πŸ”’ Frozen Dataclass Protected:", type(err).__name__, err)
πŸ” Data Integrity:

Frozen dataclasses provide thread-safe, unalterable value objects perfect for domain configuration and mathematical vectors.

⚠️ Common Developer Pitfall: Using Mutable Defaults in Dataclasses without field(default_factory=...)

Writing "tags: list = []" inside a @dataclass raises ValueError: mutable default is not allowed. You must write "tags: list = field(default_factory=list)".

πŸ’» Hands-on Interactive Practice Challenge

Create a dataclass InventoryItem with name, price, quantity, and a method total_value() returning price * quantity.

Python 3 Practice Challenge β–Ά Run in Compiler
from dataclasses import dataclass

@dataclass
class InventoryItem:
    name: str
    price: float
    quantity: int = 1

    def total_value(self) -> float:
        return self.price * self.quantity

item = InventoryItem("USB-C Hub", 1299.00, 3)
print(item)
print(f"Total Inventory Value: β‚Ή{item.total_value():,.2f}")
Run This Challenge in Online Python IDE β†’
❓ Frequently Asked Questions (FAQ)

Q When should I choose Composition over Inheritance?

Use Inheritance when there is a true polymorphic "Is-A" relationship and shared interface. Use Composition when you want to build complex behaviors by combining independent components ("Has-A") without coupling class hierarchies.

Q Can a dataclass have custom methods?

Yes! A dataclass is a regular Python class with automatically generated dunder methods. You can add methods, properties, class variables, and inheritance freely.

Q What is the __post_init__ method in a dataclass?

__post_init__() is called immediately after the generated __init__() finishes, allowing you to validate data or initialize dependent computed fields.

OC
Written by Our Compiler Technical Editorial Team
Reviewed for accuracy & tested on Python 3.12+ runtime Β· Last updated August 2026