Python Composition & Dataclasses
A famous principle in software architecture states: "Favor Object Composition over Class Inheritance".
- Inheritance ("Is-A"): Tight coupling. A
Dogis anAnimal. Changes to the parent class risk breaking all child classes across a large codebase. - Composition ("Has-A"): Loose coupling. A
Carhas anEngine. Complex systems are assembled by combining independent, reusable building block components.
# 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")
If you want an electric car, you can simply swap self.engine = ElectricMotor() without altering the vehicle's navigation, chassis, or seating logic.
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.
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!
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.
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:
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)
Frozen dataclasses provide thread-safe, unalterable value objects perfect for domain configuration and mathematical vectors.
Writing "tags: list = []" inside a @dataclass raises ValueError: mutable default
Create a dataclass InventoryItem with name, price, quantity, and a method total_value() returning price * quantity.
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}")
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.