Phase 5: Object-Oriented & Modular Thinking · ~30 minutes · Python · uv
Composition and Modular Design
A Car isn't a kind of Engine. It has one. Get that relationship backwards and your Car ends up with methods that make no sense on a car.
Hiring signal: Defaults to composition and can justify, with a real is-a/has-a test, the rare cases where inheritance is actually correct
What you will learn
- Distinguish composition ('has-a') from inheritance ('is-a')
- Explain why composition is the safer default for beginners
- Diagnose a real bug caused by inheriting a method that shouldn't exist on the subclass
- Judge whether a proposed relationship between two classes is genuinely is-a or actually has-a
Introduction
Type: Learn Languages: Python Prerequisites: Lesson 02 (Why ML Codebases Are Full of Classes) Time: ~30 minutes
Objective
Learning objectives
- Distinguish composition ("has-a") from inheritance ("is-a")
- Explain why composition is the safer default for beginners
- Diagnose a real bug caused by inheriting a method that shouldn't exist on the subclass
- Judge whether a proposed relationship between two classes is genuinely is-a or actually has-a
What you're building
Take this over-large class that's grown to do two genuinely separate jobs (validating orders and formatting receipts):
class OrderProcessor:
def __init__(self, items):
self.items = items
def is_valid(self):
return len(self.items) > 0 and all(i["price"] > 0 for i in self.items)
def total(self):
return sum(i["price"] for i in self.items)
def format_receipt(self):
lines = [f"{i['name']}: ${i['price']:.2f}" for i in self.items]
return "\n".join(lines) + f"\nTotal: ${self.total():.2f}"
A script (refactor_composition.py) that:
- Splits this into two smaller classes —
Order (holding items, is_valid(), total()) and Receipt (formatting output) — where Receipt has an Order, not inherits from one - Demonstrates the refactored version producing identical output to the original for the same input data
- Contains a comment justifying, using this lesson's is-a/has-a test, why
Receipt(Order) (inheritance) would have been the wrong choice here
A codebase has a Logger class with a .write(message) method. A developer is deciding between class ReportGenerator(Logger): and class ReportGenerator: def __init__(self): self.logger = Logger(). Which relationship is correct?
This is exactly the Car/Engine pattern again, with different names. A ReportGenerator needing to log messages is a has-a relationship — it uses a Logger as one of its tools, the same way a Car uses an Engine. Making ReportGenerator inherit from Logger would mean every ReportGenerator instance is officially considered a kind of Logger (isinstance(report_gen, Logger) would be True) and would automatically expose every one of Logger's own methods directly on ReportGenerator — exactly the kind of interface-leaking this lesson's callout warns about, for a relationship that was never genuinely is-a.
Unlock the full lesson
You've read the first 2 sections. The rest of this lesson covers The Problem, Check Yourself, Key Terms & Next — plus a hands-on lab, quiz, and project artifact.
Create a free account to unlock Phase 0 and Phase 1 of every course — no credit card.
Browse all courses · View pricing · DeVenture Academy