Python

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

JrCodex·5 min read

Jr Codex Python Notes

Level: Advanced Prerequisites: Chapter 3 Time to complete: ~20 minutes


Table of Contents

  1. "Is-A" vs "Has-A"
  2. Composition in Practice
  3. The Problem with Overusing Inheritance
  4. Refactoring Inheritance into Composition
  5. Delegation
  6. "Favor Composition Over Inheritance" — When to Actually Apply This
  7. 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 rotating
Composition 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 behavior

The 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 successful

The 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 inheritanceYou'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 (Dog is an Animal); composition models has-a relationships (Car has an Engine).
  • 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

  1. Why is class Penguin(FlyingAnimal): a design smell, even though it technically runs?
  2. In the PaymentProcessor example, what would you need to change to log to a file instead of the console?
  3. 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