Has-a versus Is-a: Why Prefer Composition
18 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Every class design starts with one question: is this object a kind of that one, or does it have that one? Inheritance answers the first. Composition answers the second, and it is usually the safer choice.
Is-a means inheritance
Is-a relationship means one class is a more specific kind of another, such as a Manager being an Employee. Python models it with inheritance: class Manager(Employee).
A Manager is an Employee. Anything you can do with an Employee, you can do with a Manager. That is the test for inheritance.
class Employee: def __init__(self, name): self.name = name
def role(self): return "Employee"
class Manager(Employee): def role(self): return "Manager"Example 1 · A Manager is an Employee
class Employee: def __init__(self, name): self.name = name
def role(self): return "Employee"
class Manager(Employee): def role(self): return "Manager"
m = Manager("Asha")print(m.name, m.role())print(isinstance(m, Employee))Asha Manager True
What this code does
Managerreusesnameand overridesrole.isinstanceis True because a Manager really is an Employee.
Use inheritance only when the child can stand in for the parent everywhere.
Has-a means composition
Composition means building an object from other objects it holds as attributes (a has-a relationship). The whole delegates work to its parts instead of inheriting from them.
A Customer is not an Address. A Customer has an Address. So the Customer keeps an Address object in an attribute and asks it for help.
Example 2 · A Customer has an Address
class Address: def __init__(self, city, pin): self.city = city self.pin = pin
def label(self): return f"{self.city} - {self.pin}"
class Customer: def __init__(self, name, address): self.name = name self.address = address
def ship_to(self): return f"{self.name}, {self.address.label()}"
c = Customer("Ravi", Address("Pune", "411001"))print(c.ship_to())print(isinstance(c, Address))Ravi, Pune - 411001 False
What this code does
ship_toasks the Address for its label instead of building the text itself.- A Customer is not an Address, so
isinstanceis False.
Example 3 · A customer has an address
class Address: def __init__(self, city, pin): self.city = city self.pin = pin
def label(self): return f"{self.city} - {self.pin}"
class Customer: def __init__(self, name, address): self.name = name self.address = address
def ship_to(self): return f"{self.name}, {self.address.label()}"
customer = Customer("Asha", Address("Pune", "411001"))print(customer.ship_to())Asha, Pune - 411001
What this code does
Customerkeeps anAddressobject inself.address.ship_toasks the address for its label instead of rebuilding it.
The Address is a part of the Customer. The Customer never inherits from it.
Why inheriting from a list goes wrong
Suppose you want a stack. Subclassing list is quick, but the stack now also has insert, remove and sort. Callers can break the stack rules.
class BadStack(list): def push(self, item): self.append(item)
stack = BadStack()stack.push(1)stack.push(2)stack.insert(0, 99) # allowed, but a stack has no bottom insertExample 4 · A composed stack hides the list
class Stack: def __init__(self): self._items = []
def push(self, item): self._items.append(item)
def pop(self): return self._items.pop()
def size(self): return len(self._items)
s = Stack()s.push(1)s.push(2)print(s.pop())print(s.size())print(hasattr(s, "insert"))2 1 False
What this code does
- The list lives in
_items, so only push, pop and size are public. hasattr(s, "insert")is False: callers cannot break the stack order.
A stack has a list. It is not a list. Holding the list privately keeps the public surface small.
Forward the call, swap the part
A Car does not need to be an Engine. It holds one and forwards start() to it. Forwarding a call to a part is called delegation. Because the part is only an attribute, you can replace it later.
class Engine: def start(self): return "engine running"
class Car: def __init__(self, engine): self.engine = engine
def start(self): return "Car: " + self.engine.start()Example 5 · Delegate to a part and swap it
class PetrolEngine: def start(self): return "petrol engine running"
class ElectricMotor: def start(self): return "electric motor running"
class Car: def __init__(self, engine): self.engine = engine
def start(self): return "Car: " + self.engine.start()
car = Car(PetrolEngine())print(car.start())car.engine = ElectricMotor()print(car.start())Car: petrol engine running Car: electric motor running
What this code does
Car.startforwards the call to whatever part it holds.- Assigning a new
car.enginechanges behaviour without a new class.
Inheritance fixes the parent when you write the class. Composition lets you choose the part when you create or change the object.
Next step
Check your understanding
10 quick questions