Python Tutorial
Composition vs Inheritance
Inheritance models an "is-a" relationship: a SavingsAccount is a BankAccount. Composition models a "has-a" relationship: a Car has an Engine, an Order has a PaymentMethod. Experienced developers follow the guideline "favour composition over inheritance", because composed objects are easier to change, test and reuse.
This lesson compares the two approaches, shows the problems of deep inheritance, and demonstrates composition, delegation and dependency injection in Python.
Problems with Overusing Inheritance
Inheritance couples a child tightly to its parent's implementation: changes in the parent ripple into every subclass. Trying to express every combination through subclasses causes an explosion of classes (EmailUrgentNotifier, SmsUrgentNotifier...). Deep hierarchies are hard to follow, and a subclass inherits everything, even behaviour it should not have.
Composition
With composition, an object holds references to other objects and delegates work to them. Behaviour can be swapped at runtime by passing a different component, each component is small and testable on its own, and combinations are formed by assembling objects instead of defining new classes.
Dependency Injection
Passing collaborators into a class (usually through __init__) instead of creating them inside is called dependency injection. It lets tests pass fakes — a fake mailer instead of a real SMTP connection — and makes dependencies explicit. Frameworks such as FastAPI and Spring build on this idea.
When Inheritance Is Right
Use inheritance when there is a genuine is-a relationship, the subclass can be used anywhere the parent is expected (the Liskov substitution principle), and you want to share an interface or template. Use composition for everything else.
Examples
Composition: a Car has an Engine
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
return f"{self.horsepower}hp engine started"
class ElectricMotor:
def start(self):
return "silent electric motor started"
class Car:
def __init__(self, model, engine):
self.model = model
self.engine = engine # has-a
def drive(self):
return f"{self.model}: {self.engine.start()}"
print(Car("Sedan", Engine(120)).drive())
print(Car("EV", ElectricMotor()).drive())
Sedan: 120hp engine started
EV: silent electric motor started
Avoiding a class explosion by composing behaviours
class EmailChannel:
def send(self, text):
return f"[email] {text}"
class SmsChannel:
def send(self, text):
return f"[sms] {text}"
class UrgentFormat:
def format(self, text):
return f"URGENT: {text.upper()}"
class PlainFormat:
def format(self, text):
return text
class Notifier:
def __init__(self, channel, formatter):
self.channel, self.formatter = channel, formatter
def notify(self, text):
return self.channel.send(self.formatter.format(text))
combos = [(EmailChannel(), PlainFormat()), (SmsChannel(), UrgentFormat()), (EmailChannel(), UrgentFormat())]
for channel, fmt in combos:
print(Notifier(channel, fmt).notify("server restarted"))
[email] server restarted
[sms] URGENT: SERVER RESTARTED
[email] URGENT: SERVER RESTARTED
Dependency injection makes testing easy
class SmtpMailer:
def send(self, to, body):
raise RuntimeError("would contact a real mail server")
class FakeMailer:
def __init__(self):
self.sent = []
def send(self, to, body):
self.sent.append((to, body))
class RegistrationService:
def __init__(self, mailer):
self.mailer = mailer # injected dependency
def register(self, email):
self.mailer.send(email, "Welcome to Webnest!")
return f"registered {email}"
fake = FakeMailer()
service = RegistrationService(fake)
print(service.register("asha@webnest.in"))
print(fake.sent)
registered asha@webnest.in
[('asha@webnest.in', 'Welcome to Webnest!')]
Common Mistakes
- Using inheritance just to reuse a helper method.
- Creating a subclass for every combination of features.
- Creating dependencies inside __init__ (self.mailer = SmtpMailer()), making classes hard to test.
- Subclasses that break the parent's contract, so they cannot replace it safely.
Key Points to Remember
- Inheritance = is-a; composition = has-a.
- Favour composition: smaller, swappable, testable components.
- Compose behaviours instead of creating a subclass per combination.
- Inject dependencies through __init__ to decouple and test easily.
- Use inheritance for genuine is-a relationships that honour the parent's contract.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.