Step 1 Understand it
The problem it solves. looks like free reuse, but every is tied to its parent's details. With several independent features, the number of subclasses explodes.
Watch it happen
Read the animation as text
- With inheritance, every combination of behaviours needs its own subclass: 3 ways to fly × 3 sounds = 9 classes — and every new behaviour multiplies them again.
- With composition, a Duck simply HOLDS a flying object and a quacking object. Each behaviour is a small class behind a tiny interface: fly() or quack().
- Build a duck by choosing its parts: Duck(Wings(), Quack()). Six small classes cover all nine combinations.
- A rubber duck is Duck(NoFly(), Squeak()) — no new subclass needed.
- You can even swap a part while the program runs — strap a Rocket on — and test each part on its own. Inherit only for a true "is-a" with shared behaviour.
Picture it
classDiagram
class Duck {
+perform()
}
class Flyer {
<<interface>>
+fly()
}
class Quacker {
<<interface>>
+quack()
}
Duck o-- Flyer : has a
Duck o-- Quacker : has a
Flyer <|.. Wings
Flyer <|.. NoFly
Quacker <|.. SqueakThe idea
Inheritance ties a child class closely to its parent's internals, so changes ripple down and deep trees get rigid. plugs behaviour in through small objects behind , so you can swap them at runtime and test each alone. Rule of thumb: inherit for a true 'is-a' with shared behaviour; otherwise, compose.
How it works, step by step
- With inheritance, 3 kinds of flying × 3 kinds of sound = 9 subclasses, and every new kind multiplies them again.
- With composition, a Duck simply holds a flyer object and a quacker object.
- Each behaviour is a small implementing a tiny interface: fly() or quack().
- You build a duck by choosing its parts: Duck(Wings(), Quack()) or Duck(NoFly(), Squeak()).
- You can even swap a part while the program runs, and test each part on its own.
Analogy
A phone with a replaceable camera module, compared with a phone whose camera is moulded into the case. Upgrading the module is easy; re-moulding the case isn't.
Where the analogy breaks
Phone modules have sockets designed in advance. In code you still have to choose good interfaces, and composing everything everywhere can create a maze of tiny objects.
Example
Instead of Duck → FlyingDuck → QuackingFlyingDuck classes, a Duck has a flying behaviour and a quacking behaviour. A rubber duck gets NoFly and Squeak.
Must-know points
- 'Is-a' → maybe inheritance. 'Has-a' → composition.
- Composition lets you change behaviour at runtime and test with fakes.
- Program to an interface, not to an implementation.
- : pass collaborators in; don't create them inside the class.
Common mistakes
- Using inheritance for 'has-a' relationships (a Car extends Engine).
- Creating collaborators inside the class instead of passing them in.
class Duck:
def __init__(self, flyer, quacker):
self.flyer, self.quacker = flyer, quacker # has-a, not is-a
def perform(self):
return f"{self.flyer.fly()} / {self.quacker.quack()}"
class Wings:
def fly(self): return "flaps its wings"
class NoFly:
def fly(self): return "can't fly"
class Squeak:
def quack(self): return "squeak"
rubber_duck = Duck(NoFly(), Squeak())
rubber_duck.perform() # "can't fly / squeak"Step 2 Go further (free, optional)
Optional. The notes above are written to be enough on their own. These free resources give a second explanation and extra practice.
-
Read
Inheritance and Composition: A Python OOP Guide
Real Python
-
Read
Refactoring.Guru, free
Step 3 Practise
Try each exercise yourself first: on paper, out loud or in code. Then open the answer and compare it with yours.
-
ExplainSay it in plain words in under a minute, with no notes.
Explain composition over inheritance with the phone-camera picture.
Show answer
Two phones. In the first, the camera is moulded into the case. To get a better camera you need a whole new phone: that is inheritance, where behaviour is baked into the class, and changing it means a new subclass. In the second, the camera is a module that clicks into a standard slot. To upgrade, you swap the module; the rest of the phone doesn't change, and you can test a module on its own. That is composition: the object holds a separate part, behind an interface (the slot), and the part can be swapped, even while the program runs.
With inheritance, every combination of features needs its own class (FlyingQuackingDuck, FlyingSqueakingDuck…). With composition, you combine small parts freely: a duck has a way of flying and a way of quacking.
-
DrawSketch the diagram (or class diagram) from memory.
Draw the Duck design with its behaviour objects as a .
Show answer
The Duck has its two behaviours (the diamond means it holds them). Each behaviour is an interface, and the small classes below implement it (the dotted line with a hollow triangle). A rubber duck is just Duck(NoFly(), Squeak()): no new subclass needed.
textDuck - flyer: FlyBehaviour - quacker: QuackBehaviour + perform() ◆ has a ◆ has a │ │ ▼ ▼ «interface» «interface» FlyBehaviour QuackBehaviour + fly() + quack() △ △ △ △ ┊ ┊ ┊ ┊ Wings NoFly Quack Squeak -
ApplyUse it in a real design question and say exactly where it fits.
Refactor a Notifier hierarchy (EmailNotifier, SmsNotifier, EmailAndSmsNotifier) into one Notifier that holds a list of channels.
Show answer
With inheritance, every mix of channels needs its own class: with email, SMS and push, that is up to 7 classes (every non-empty combination of 3). With composition, there is one Notifier holding a list of channels, and one small class per channel. Any combination is just a different list, and a new channel is one new class.
pythonfrom abc import ABC, abstractmethod class Channel(ABC): @abstractmethod def send(self, user, message): ... class EmailChannel(Channel): def send(self, user, message): return f"email to {user}: {message}" class SmsChannel(Channel): def send(self, user, message): return f"sms to {user}: {message}" class Notifier: def __init__(self, channels): self.channels = list(channels) # has many channels def notify(self, user, message): return [channel.send(user, message) for channel in self.channels] print(Notifier([EmailChannel(), SmsChannel()]).notify("priya", "Your order shipped")) # ['email to priya: Your order shipped', 'sms to priya: Your order shipped'] class PushChannel(Channel): # a new channel: no combination classes def send(self, user, message): return f"push to {user}: {message}" print(Notifier([SmsChannel(), PushChannel()]).notify("tom", "Code 4821")) # ['sms to tom: Code 4821', 'push to tom: Code 4821'] -
Trade-offsSay what it costs, when not to use it, and the alternative.
When is inheritance still the better choice?
Show answer
When there is a true "is a kind of" relationship, the child can keep every promise the parent makes, and they share real behaviour:
• Exception classes: class PaymentDeclined(PaymentError). Code that catches PaymentError should catch every kind of payment error. This is exactly what inheritance is for.
• Framework extension points: a test class extends unittest.TestCase, or a web view extends the framework's base view. The framework is designed to be extended this way and calls your overrides.
• Template method: the parent fixes the steps of an algorithm and the children fill in one or two of them (for example, a report that always loads, formats and saves, where only the formatting differs).
• Shallow, stable hierarchies you own: one or two levels, unlikely to grow.The warning signs that composition would be better: a hierarchy getting deeper, classes for combinations of features, or a child overriding a method to do nothing.
Step 4 Interview-ready, then prove it
Your interview answer
About 20 to 40 seconds, said out loud. Practise it until it sounds like you, not like notes.
I prefer composition: a class holds small collaborator objects behind interfaces, and they're injected. It's more flexible than inheritance, easier to test, and avoids fragile class trees. I use inheritance only for a genuine 'is-a' with shared behaviour.
Follow-up questions
What is dependency injection?
Giving an object its dependencies from outside, usually through the constructor, instead of creating them inside — so you can swap them, for example a fake payment in tests.
What is the 'fragile base class' problem?
A change to a parent class unexpectedly breaks subclasses that relied on its internal behaviour.
Tick each check when you can do it without notes
A topic is finished when you pass all four checks, not when you finish a video. Be strict: if you needed your notes, it is not ticked yet.
- Explain Say it in plain words in under a minute, with no notes.
- Draw Sketch the diagram (or class diagram) from memory.
- Apply Use it in a real design question and say exactly where it fits.
- Trade-offs Say what it costs, when not to use it, and the alternative.
0 of 4 checks ticked.
Saved in this browser. Back it up or move it to another device.