Object Oriented Programming
Composition vs Inheritance
Chapter 2 modeled relationships with inheritance: a Dog is an Animal. But not every relationship fits that mold. Composition models a different, equally common
Jr Codex Python Notes
Level: Advanced Prerequisites: Chapter 3 Time to complete: ~20 minutes
Table of Contents
- "Is-A" vs "Has-A"
- Composition in Practice
- The Problem with Overusing Inheritance
- Refactoring Inheritance into Composition
- Delegation
- "Favor Composition Over Inheritance" — When to Actually Apply This
- Summary & Next Steps
1. "Is-A" vs "Has-A"
Chapter 2 modeled relationships with inheritance: a Dog is an Animal. But not every relationship fits that mold. Composition models a different, equally common relationship: an object has a part.
Is-A (Inheritance) Has-A (Composition)
───────────────────────────── ─────────────────────────────
Dog IS-A Animal Car HAS-A Engine
Circle IS-A Shape Car HAS-A set of Wheels
Puppy IS-A Dog House HAS-A Kitchen
───────────────────────────── ─────────────────────────────
A Car is not a kind of Engine — it doesn't make sense to say Car(Engine). Instead, a Car contains an Engine object as one of its parts.
2. Composition in Practice
Composition just means one object stores another object as an attribute, and delegates work to it:
class Engine:
def __init__(self, horsepower):
self.horsepower = horsepower
def start(self):
print(f"Engine starting... {self.horsepower} HP roaring to life")
class Wheels:
def __init__(self, count=4):
self.count = count
def rotate(self):
print(f"{self.count} wheels rotating")
class Car:
def __init__(self, make, horsepower):
self.make = make
self.engine = Engine(horsepower) # Car HAS-A Engine
self.wheels = Wheels(4) # Car HAS-A Wheels
def start(self):
print(f"Starting {self.make}...")
self.engine.start() # delegate to the Engine object
self.wheels.rotate() # delegate to the Wheels object
my_car = Car("Toyota", 300)
my_car.start()
# Starting Toyota...
# Engine starting... 300 HP roaring to life
# 4 wheels rotatingComposition Structure
─────────────────────────────────────────
Car
├── engine → an Engine object
└── wheels → a Wheels object
Car doesn't inherit Engine's behavior — it OWNS an Engine
and calls INTO it when needed.
─────────────────────────────────────────
3. The Problem with Overusing Inheritance
Inheritance hierarchies can become rigid and awkward when relationships don't cleanly fit "is-a" — a classic example:
class FlyingAnimal:
def fly(self):
print("Flying!")
class Bird(FlyingAnimal):
pass
class Penguin(FlyingAnimal): # ✗ problem: penguins can't fly!
pass
penguin = Penguin()
penguin.fly() # "Flying!" — WRONG, but the class hierarchy forces this behaviorThe deeper issue: Penguin(FlyingAnimal) claims "a Penguin IS-A FlyingAnimal," which is factually wrong, just because both are birds. Forcing a hierarchy onto something that doesn't cleanly fit leads to broken assumptions like this.
4. Refactoring Inheritance into Composition
class FlightBehavior:
def move(self):
print("Flying through the air!")
class SwimBehavior:
def move(self):
print("Swimming through the water!")
class Bird:
def __init__(self, name, movement):
self.name = name
self.movement = movement # HAS-A movement behavior, chosen per instance
def move(self):
print(f"{self.name}: ", end="")
self.movement.move()
sparrow = Bird("Sparrow", FlightBehavior())
penguin = Bird("Penguin", SwimBehavior())
sparrow.move() # Sparrow: Flying through the air!
penguin.move() # Penguin: Swimming through the water!Now each Bird is composed with whatever movement behavior is actually true for it, instead of being locked into an all-or-nothing inheritance claim. This pattern (injecting behavior as an object, rather than baking it into a class hierarchy) is called the Strategy pattern.
5. Delegation
Delegation is the technique composition relies on: instead of implementing behavior directly, an object forwards ("delegates") the work to an object it holds.
class Logger:
def log(self, message):
print(f"[LOG] {message}")
class PaymentProcessor:
def __init__(self):
self.logger = Logger() # composition: HAS-A Logger
def process_payment(self, amount):
self.logger.log(f"Processing payment of ${amount}") # delegate the logging work
# ... actual payment logic here ...
self.logger.log("Payment successful")
processor = PaymentProcessor()
processor.process_payment(99.99)
# [LOG] Processing payment of $99.99
# [LOG] Payment successfulThe big practical win: swap in a different Logger (writes to a file, sends to a monitoring service) without touching PaymentProcessor at all — as long as the replacement has a compatible .log() method (duck typing, Chapter 3).
6. "Favor Composition Over Inheritance" — When to Actually Apply This
This is a well-known object-oriented design principle, but it doesn't mean "never use inheritance" — Chapters 1–3 relied on inheritance productively. The guideline is about picking the right tool per relationship:
| Use Inheritance When... | Use Composition When... |
|---|---|
The relationship is genuinely "is-a" and stable (a Dog really is an Animal, always) | The relationship is "has-a," or "is-a" would force a false claim (a Penguin "is-a" FlyingAnimal? No.) |
You want to share a common interface across related types (Chapter 3's Shape example) | You want to swap out a piece of behavior at runtime, or reuse a helper across otherwise-unrelated classes |
| The hierarchy is shallow and unlikely to need deep, awkward multiple inheritance | You'd otherwise need multiple inheritance to combine unrelated behaviors |
Quick Gut-Check
─────────────────────────────────────────
Can you honestly say "X IS-A Y" and have it hold true FOREVER?
→ Yes: inheritance is reasonable
→ No, or "sometimes": composition is safer
─────────────────────────────────────────
7. Summary & Next Steps
Key Takeaways
- Inheritance models is-a relationships (
Dogis anAnimal); composition models has-a relationships (Carhas anEngine). - Composition works through delegation — an object holds another object and forwards work to it, rather than inheriting that behavior directly.
- Overusing inheritance for relationships that don't cleanly fit "is-a" (like
Penguin(FlyingAnimal)) leads to broken, factually-wrong hierarchies. - "Favor composition over inheritance" is a guideline, not an absolute rule — use inheritance for genuine, stable "is-a" relationships and shared interfaces; use composition when behavior needs to be swapped or combined flexibly.
Concept Check
- Why is
class Penguin(FlyingAnimal):a design smell, even though it technically runs? - In the
PaymentProcessorexample, what would you need to change to log to a file instead of the console? - What's the "gut-check" question for deciding between inheritance and composition?
Next Chapter
→ Chapter 5: Encapsulation & @property
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index