Skip to main content
Updated Feb 26, 2026

Composition Over Inheritance and Code Organization

In Lessons 1 and 2, you learned that inheritance is powerful—it lets you build hierarchies of classes where specialized types inherit from general types. Dog is-a Animal. ElectricCar is-a Car. Inheritance feels natural for modeling these relationships.

But here's the professional secret: most of the time, composition is better than inheritance.

This might seem contradictory. You just spent a lesson mastering inheritance! But professional developers have learned through hard experience that inheritance creates rigid class hierarchies that are difficult to change. Composition—the "has-a" relationship—creates flexible, decoupled designs that evolve as requirements change.

In this lesson, you'll learn when to choose composition over inheritance, how to design systems that are easy to modify, and how to organize your code into modules and packages for real-world projects. By the end, you'll understand why experienced architects prefer composition and how to structure Python projects professionally.


Composition: The "Has-A" Relationship

Composition means building classes by combining other objects. Instead of inheriting behavior, a class contains other objects and delegates work to them.

Side-by-side comparison of composition versus inheritance design patterns showing when to use has-a relationships with flexible component composition versus is-a relationships with rigid class hierarchies

Let's start with a concrete example. Imagine designing a Car class:

class Engine:
"""A Car has-an Engine"""

def __init__(self, horsepower: int) -> None:
self.horsepower = horsepower

def start(self) -> str:
return f"Engine with {self.horsepower}hp started"

def stop(self) -> str:
return "Engine stopped"


class Car:
"""A Car has-an Engine, not IS-AN Engine"""

def __init__(self, make: str, engine: Engine) -> None:
self.make = make
self.engine = engine # Composition: Car HAS-AN Engine

def start(self) -> str:
return f"{self.make}: {self.engine.start()}"

def stop(self) -> str:
return f"{self.make}: {self.engine.stop()}"


# Create an engine and give it to a car
engine = Engine(200)
car = Car("Toyota", engine)
print(car.start()) # Toyota: Engine with 200hp started
print(car.stop()) # Toyota: Engine stopped

Notice: Car doesn't inherit from Engine. Instead, Car has an Engine as an attribute. When you call car.start(), the car delegates to its engine's start() method.

🎓 Expert Insight

In AI-native development, composition is the default pattern. Multi-agent systems use composition—an orchestrator agent has specialized sub-agents. Understanding composition is more critical than mastering inheritance hierarchies.


Why Composition Wins Over Inheritance

Let's see why professionals prefer composition. Consider the design problem: A penguin is a bird, but penguins can't fly.

Using inheritance (the wrong way):

class Bird:
def fly(self) -> str:
return "Flying high!"


class Penguin(Bird):
# Problem: Penguin inherits fly() but can't actually fly!
pass


penguin = Penguin()
print(penguin.fly()) # This is wrong - penguins don't fly!

This breaks the Liskov Substitution Principle: a subclass should be usable wherever the parent is used. But a Penguin can't reliably replace a Bird—it can't fly!

Using composition (the right way):

class Flyer:
"""Capability: ability to fly"""
def fly(self) -> str:
return "Flying!"


class Swimmer:
"""Capability: ability to swim"""
def swim(self) -> str:
return "Swimming!"


class Bird:
"""A bird has capabilities"""
def __init__(self, name: str, flyer: Flyer | None = None, swimmer: Swimmer | None = None) -> None:
self.name = name
self.flyer = flyer
self.swimmer = swimmer

def move(self) -> str:
if self.flyer:
return f"{self.name}: {self.flyer.fly()}"
elif self.swimmer:
return f"{self.name}: {self.swimmer.swim()}"
return f"{self.name}: Walking"


class Penguin(Bird):
"""Penguins have swimming but not flying"""
def __init__(self, name: str) -> None:
super().__init__(name, flyer=None, swimmer=Swimmer())


# This is correct - penguin swims, doesn't fly
penguin = Penguin("Pingu")
print(penguin.move()) # Pingu: Swimming!

Now the design is flexible. Penguins and eagles have different capabilities without forcing an inheritance hierarchy.

💬 AI Colearning Prompt

"Ask your AI: Why do professional developers prefer 'composition over inheritance'? Give me 5 concrete reasons with code examples showing when inheritance fails and composition succeeds."


Aggregation vs Composition: Understanding Coupling

Both composition and aggregation are "has-a" relationships, but they differ in coupling—how tightly objects are bound together.

Composition: Strong Ownership

In composition, the container creates and owns the contained object. When the container dies, the contained object dies with it.

class Car:
def __init__(self, make: str, horsepower: int) -> None:
self.make = make
self.engine = Engine(horsepower) # Car CREATES and OWNS engine

# If Car is destroyed, Engine is destroyed


# The engine's lifetime is tied to the car's
car = Car("Toyota", 200)
# If car is deleted, engine is also deleted (both go away together)

Aggregation: Weak Relationship

In aggregation, the container references but doesn't own the contained object. The contained object can exist independently.

class Department:
def __init__(self, name: str) -> None:
self.name = name


class University:
def __init__(self, name: str) -> None:
self.name = name
self.departments: list[Department] = []

def add_department(self, dept: Department) -> None:
self.departments.append(dept) # University REFERENCES, doesn't create


# Department can exist independently
cs_dept = Department("Computer Science")
university = University("Tech University")
university.add_department(cs_dept)

# Even if university closes, cs_dept can continue to exist
# (The relationship is weak)

🚀 CoLearning Challenge

Ask your AI: "I have a Library with Books. Should I use composition or aggregation? Books are created in the library's catalog system but might be shared or archived. What pattern fits?"


Organizing Code into Modules and Packages

As your projects grow beyond single files, you need to organize classes into logical modules. Python's module system lets you split code across files and organize files into packages (directories with __init__.py).

Module: A File with Classes

Create a file animals.py with animal-related classes:

# animals.py
class Dog:
def __init__(self, name: str) -> None:
self.name = name

def speak(self) -> str:
return f"{self.name} says: Woof!"


class Cat:
def __init__(self, name: str) -> None:
self.name = name

def speak(self) -> str:
return f"{self.name} says: Meow!"

In another file, import and use these classes:

# main.py
from animals import Dog, Cat

dog = Dog("Buddy")
cat = Cat("Whiskers")

print(dog.speak()) # Buddy says: Woof!
print(cat.speak()) # Whiskers says: Meow!

Package: A Directory with __init__.py

For larger projects, organize modules into packages:

my_project/
├── animals/
│ ├── __init__.py
│ ├── mammals.py
│ └── birds.py
├── vehicles/
│ ├── __init__.py
│ └── cars.py
└── main.py

animals/mammals.py:

class Dog:
def speak(self) -> str:
return "Woof!"


class Cat:
def speak(self) -> str:
return "Meow!"

animals/init.py (controls public API):

from .mammals import Dog, Cat
from .birds import Parrot

# __all__ controls what gets imported with "from animals import *"
__all__ = ['Dog', 'Cat', 'Parrot']

main.py (uses the package):

from animals import Dog, Cat, Parrot

dog = Dog()
cat = Cat()
parrot = Parrot()

The __init__.py file is critical—it tells Python "this directory is a package" and controls which classes are publicly available through the package name.

✨ Teaching Tip

Use Claude Code to explore real projects: "Show me how Django organizes its apps, models, views, and templates. How does it use packages and init.py to create a scalable architecture?"


Refactoring Inheritance to Composition: A Real Example

Let's see how to recognize when inheritance is wrong and refactor to composition.

Problematic Inheritance Design:

# This inheritance hierarchy is rigid
class Employee:
def __init__(self, name: str, salary: float) -> None:
self.name = name
self.salary = salary


class Manager(Employee):
"""Manager IS-AN Employee with extra responsibilities"""
def __init__(self, name: str, salary: float, team_size: int) -> None:
super().__init__(name, salary)
self.team_size = team_size


class Printer(Employee):
"""Wait... Employee IS-A Printer? This makes no sense!"""
def print_report(self) -> str:
return f"Printing {self.name}'s report"


# The design is confused. Not all employees print reports.

Refactored with Composition:

class Employee:
def __init__(self, name: str, salary: float) -> None:
self.name = name
self.salary = salary


class ReportPrinter:
"""A tool for printing reports"""
def print_report(self, employee: Employee) -> str:
return f"Printing {employee.name}'s report"


class Manager(Employee):
"""Manager IS-AN Employee with team oversight"""
def __init__(self, name: str, salary: float, team_size: int) -> None:
super().__init__(name, salary)
self.team_size = team_size


# Optional: Manager has a printer tool
manager = Manager("Alice", 100000, 5)
printer = ReportPrinter()
print(printer.print_report(manager)) # Printing Alice's report

Now the design makes sense:

  • Manager IS-AN Employee (inheritance for real "is-a" relationships)
  • Manager HAS-A Printer (composition for optional capabilities)

🎓 Expert Insight

The rule: Inheritance models unchanging identity ("is-a"), composition models changeable capabilities ("has-a"). An object's type rarely changes, but its capabilities often do.


Real-World Project Structure: Multi-Agent System

Here's how a professional project organizing multiple agent types might look:

ai_agent_system/
├── agents/
│ ├── __init__.py
│ ├── base.py # Abstract Agent base class
│ ├── chat_agent.py # ChatAgent (HAS-A reasoning engine)
│ └── code_agent.py # CodeAgent (HAS-A syntax checker)
├── engines/
│ ├── __init__.py
│ ├── reasoning.py # Reasoning engine (composition)
│ └── syntax_checker.py # Syntax validation (composition)
├── events/
│ ├── __init__.py
│ └── bus.py # Event bus for agent communication
└── main.py # Orchestration

agents/base.py (abstract base):

from abc import ABC, abstractmethod


class Agent(ABC):
"""All agents share this interface"""

def __init__(self, name: str) -> None:
self.name = name

@abstractmethod
def process(self, message: str) -> str:
pass

agents/chat_agent.py (uses composition):

from agents.base import Agent
from engines.reasoning import ReasoningEngine


class ChatAgent(Agent):
"""ChatAgent HAS-A ReasoningEngine (composition)"""

def __init__(self, name: str, reasoning_engine: ReasoningEngine) -> None:
super().__init__(name)
self.reasoning_engine = reasoning_engine # Composition!

def process(self, message: str) -> str:
reasoning = self.reasoning_engine.reason(message)
return f"{self.name}: {reasoning}"

agents/init.py (public API):

from .base import Agent
from .chat_agent import ChatAgent
from .code_agent import CodeAgent

__all__ = ['Agent', 'ChatAgent', 'CodeAgent']

main.py (orchestration):

from agents import ChatAgent, CodeAgent
from engines.reasoning import ReasoningEngine
from engines.syntax_checker import SyntaxChecker

# Create engines (shared across agents or individual)
reasoning = ReasoningEngine()
syntax = SyntaxChecker()

# Create agents with their tools (composition)
chat = ChatAgent("ChatBot", reasoning)
code = CodeAgent("CodeHelper", syntax)

# Use agents
print(chat.process("Explain Python"))
print(code.process("Check this function"))

This design is flexible: agents are composed from interchangeable engines. You can swap engines, test with mock engines, and add new agent types without modifying existing code.


Key Design Principles: When to Use Each Pattern

Use Inheritance for:

  • Permanent, fundamental "is-a" relationships
  • Examples: Dog is-an Animal, Circle is-a Shape, Manager is-an Employee
  • The relationship doesn't change

Use Composition for:

  • Changeable capabilities and relationships
  • Examples: Car has-an Engine, Agent has-a ReasoningEngine, Employee has-a Printer
  • The relationship can be modified, swapped, or removed

The Liskov Substitution Principle Test:

  • If a subclass can't reliably replace the parent (like Penguin can't replace Bird), use composition instead

💬 AI Colearning Prompt

"Ask your AI: Design a game with Players, Weapons, and Armor. Should Player inherit from Weapon? Should Player have-a Weapon? Show me the composition design and explain why it's more flexible than inheritance."


Challenge: Building Flexible Agent Architectures with Composition

In this challenge, you'll discover why inheritance creates rigid designs, learn how composition provides flexibility, challenge AI with design questions, and build a production agent system.


Part 1: Experience Inheritance Rigidity in Agent Design

Your Role: System architect identifying design constraints with AI collaboration

Discovery Exercise: Exploring Inheritance Limitations for Capability Mixing

Imagine you're building an agent framework where agents need different capabilities. You try to model this with inheritance and discover its limitations.

💬 AI CoLearning Prompt - Discovering the Inheritance Rigidity Problem

"I'm building agents with different capabilities:

  • LLMAgent (has reasoning capability)
  • DatabaseAgent (has query capability)
  • SearchAgent (has web search capability)

I want an agent with LLM + Database capabilities. Show me how to do this with multiple inheritance (CombinedAgent inherits from LLMAgent and DatabaseAgent).

Then analyze the explosion problem:

  1. How many classes do I need for all 2-capability combinations of 3 capabilities?
  2. What about 3-capability combinations?
  3. If I have 5 capabilities (LLM, Database, Search, FileIO, CodeExec), how many combination classes?
  4. Why is this inheritance approach unsustainable?"

Expected Understanding: AI will show you that capability combinations explode combinatorially. 5 capabilities = 31 possible combinations (2^5 - 1), requiring 31 classes! This is the inheritance rigidity problem.

💬 AI CoLearning Prompt - Understanding the Modification Problem

"In my inheritance-based agent system:

  • Each capability combination = separate class
  • Example: LLM+Database = CombinedAgent class

Explain the modification problem:

  1. If I need to add logging to ALL agents (new shared behavior), how many classes need modification?
  2. If I want to add/remove a capability from an existing agent AT RUNTIME, is this possible with inheritance?
  3. Why does inheritance lock in capability combinations at class definition time?
  4. What's the difference between 'is-a' (inheritance) vs 'has-a' (composition) relationships?"

Expected Understanding: AI will explain that inheritance is static - you can't change an object's class at runtime. Adding new shared behavior requires modifying every combination class. Inheritance models identity ("is-a"), not capabilities ("has-a").

💬 AI CoLearning Prompt - Previewing the Composition Solution

"You showed me the inheritance rigidity problem. Now preview composition:

  1. What is composition? How is 'has-a' different from 'is-a'?
  2. Show me an Agent class that accepts engines as parameters (LLMEngine, DatabaseEngine, SearchEngine)
  3. How do I create an agent with LLM + Database capabilities using composition?
  4. With 5 engine types, how many Agent classes do I need? (Hint: just 1!)
  5. Can I add/remove capabilities at runtime with composition?

Show me the code difference between inheritance approach (31 classes) vs composition approach (1 Agent class + 5 engine classes)."

Expected Understanding: AI will show you that composition creates flexibility. One Agent class composed from engines can handle any capability combination. Adding capabilities at runtime is trivial: just add/remove engines from the agent's engines dict.


Part 2: Learn Composition as the Flexible Solution

Your Role: Student learning from AI Teacher

AI Teaching Prompt

Ask your AI companion:

"I'm building an agent framework. Agents need different capabilities: some have LLM reasoning, some have database access, some have web search. I tried inheritance (LLMAgent, DatabaseAgent, LLMDatabaseAgent, etc.) but it explodes combinatorially.

Explain:

  1. What is composition? How is it different from inheritance?
  2. Design an agent system where agents are composed from capability objects (ReasoningEngine, DatabaseEngine, SearchEngine)
  3. Show me how to add a new agent with new capabilities WITHOUT creating a new class
  4. What are the trade-offs between inheritance and composition?"

Expected AI Response Summary

AI will explain:

  • Composition: Objects contain other objects; capabilities come from components, not class hierarchy
  • Flexible design: New agents are created by combining engines, not creating new subclasses
  • Dynamic capabilities: Add/remove capabilities at runtime by adding/removing components
  • Scalability: 5 engines can combine into unlimited agent types without creating new classes
  • Trade-off: Inheritance enforces contracts; composition requires more careful design

AI will show code like:

# SOLUTION: Composition
class ReasoningEngine:
def reason(self, question: str) -> str:
return f"Reasoning: {question}"

class DatabaseEngine:
def query(self, query: str) -> str:
return f"Query result: {query}"

class Agent:
def __init__(self, name: str, engines: dict):
self.name = name
self.engines = engines # HAS-A engines, not IS-A subclass

def get_capability(self, capability_name: str):
return self.engines.get(capability_name)

# Create agents with different capability combinations
reasoning_agent = Agent("Reasoner", {
"reasoning": ReasoningEngine()
})

combined_agent = Agent("Combined", {
"reasoning": ReasoningEngine(),
"database": DatabaseEngine()
})

Convergence Activity

After AI explains, verify understanding:

"Show me how this composition design makes it easy to add 5 new agents with different capability combinations. Explain why inheritance couldn't handle this as elegantly. What happens if I want agents to share the same engine instance?"

Deliverable

Write 1-paragraph summary: "How Composition Replaces Inheritance in Agent Design" explaining the core insight about flexibility and scalability.


Part 3: Challenge AI with Architecture Edge Cases

Your Role: Student testing AI's understanding

Challenge Design Scenarios

Ask AI to handle these cases:

Challenge 1: Shared Engines

"If multiple agents share the same ReasoningEngine instance, what happens when one agent modifies engine state? How is this different from inheritance where each subclass might override methods? Which is safer?"

Expected learning: AI explains state management in composition vs method overriding in inheritance.

Challenge 2: Engine Dependency Chain

"What if ReasoningEngine depends on DatabaseEngine? I have Agent A with both, and Agent B with only DatabaseEngine. Show me how to wire dependencies correctly. What problems could occur?"

Expected learning: AI explains dependency injection and how composition manages complex dependencies.

Challenge 3: Module Organization

"I have an e-commerce system: products, orders, payments, shipping. Should some of these be capabilities (composition) and others base classes (inheritance)? Design the module structure with init.py files."

Expected learning: AI shows how to mix inheritance (for stable hierarchies) with composition (for flexible capabilities) and organize into packages.

Deliverable

Document your three challenges, AI's responses, and analysis of when composition beats inheritance and how to organize large systems with modules.


Part 4: Build Flexible Agent System with Composition and Modules

Your Role: Knowledge synthesizer creating reusable code

Your Modular Agent System

Create a professional project structure:

agent_system/
├── agents/
│ ├── __init__.py
│ └── agent.py # Flexible Agent class
├── engines/
│ ├── __init__.py
│ ├── reasoning.py # ReasoningEngine (composition)
│ ├── database.py # DatabaseEngine (composition)
│ └── search.py # SearchEngine (composition)
├── config.py # Agent configurations
└── main.py # Usage examples

engines/reasoning.py:

class ReasoningEngine:
"""Provides reasoning capability - composed into agents"""

def reason(self, question: str) -> str:
"""Process a reasoning question"""
return f"Reasoning about: {question}"


class DatabaseEngine:
"""Provides database capability - composed into agents"""

def query(self, sql: str) -> str:
"""Execute database query"""
return f"Query executed: {sql}"


class SearchEngine:
"""Provides web search capability - composed into agents"""

def search(self, query: str) -> str:
"""Search the web"""
return f"Search results for: {query}"

agents/agent.py:

from typing import Any


class Agent:
"""Flexible agent class composed from capability engines"""

def __init__(self, name: str, engines: dict[str, Any]) -> None:
"""
Create an agent with specific engines

Args:
name: Agent name
engines: Dict mapping capability names to engine instances
"""
self.name = name
self.engines = engines

def has_capability(self, capability: str) -> bool:
"""Check if agent has a capability"""
return capability in self.engines

def execute_reasoning(self, question: str) -> str:
"""Execute reasoning if available"""
if self.has_capability("reasoning"):
return self.engines["reasoning"].reason(question)
raise AttributeError(f"{self.name} has no reasoning capability")

def execute_query(self, sql: str) -> str:
"""Execute database query if available"""
if self.has_capability("database"):
return self.engines["database"].query(sql)
raise AttributeError(f"{self.name} has no database capability")

def execute_search(self, query: str) -> str:
"""Execute web search if available"""
if self.has_capability("search"):
return self.engines["search"].search(query)
raise AttributeError(f"{self.name} has no search capability")

def get_capabilities(self) -> list[str]:
"""List all capabilities"""
return list(self.engines.keys())

agents/init.py:

from .agent import Agent

__all__ = ['Agent']

engines/init.py:

from .reasoning import ReasoningEngine
from .database import DatabaseEngine
from .search import SearchEngine

__all__ = ['ReasoningEngine', 'DatabaseEngine', 'SearchEngine']

config.py:

"""Pre-configured agent types"""
from agents import Agent
from engines import ReasoningEngine, DatabaseEngine, SearchEngine


def create_reasoning_agent(name: str) -> Agent:
"""Agent with reasoning only"""
return Agent(name, {
"reasoning": ReasoningEngine()
})


def create_database_agent(name: str) -> Agent:
"""Agent with database access only"""
return Agent(name, {
"database": DatabaseEngine()
})


def create_full_agent(name: str) -> Agent:
"""Agent with all capabilities"""
return Agent(name, {
"reasoning": ReasoningEngine(),
"database": DatabaseEngine(),
"search": SearchEngine()
})

main.py:

from config import create_reasoning_agent, create_database_agent, create_full_agent

# Create agents with different capability combinations
reasoner = create_reasoning_agent("Thinker")
analyst = create_database_agent("Analyst")
researcher = create_full_agent("Researcher")

# Each agent has only its needed capabilities
print(f"Reasoner capabilities: {reasoner.get_capabilities()}")
print(f"Analyst capabilities: {analyst.get_capabilities()}")
print(f"Researcher capabilities: {researcher.get_capabilities()}")

# Execute capability-specific operations
print(f"Reasoner: {reasoner.execute_reasoning('What is AI?')}")
print(f"Analyst: {analyst.execute_query('SELECT * FROM data')}")
print(f"Researcher: {researcher.execute_search('latest AI trends')}")

# Adding new agents is trivial - just combine existing engines!

Your task: Expand this system with:

  1. Add 2-3 more engine types (FileEngine, APIEngine, CodeExecutionEngine)
  2. Create new agent configurations combining different engines
  3. Add a comparison document: composition_vs_inheritance_guide.md
  4. Create a module organization guide: project_structure.md

Validation Checklist

  • ✅ Agents are composed from engines, not inherited
  • ✅ New agent types require only new configurations, not new classes
  • ✅ Engines are reusable across agents
  • ✅ Adding a new engine doesn't require modifying Agent or existing engines
  • ✅ Module structure is clear with logical packages
  • init.py files define public APIs

Deliverable

Complete agent system with:

  • Modular organization (agents/, engines/, config.py)
  • Flexible Agent class using composition
  • Multiple engine types demonstrating capability-based design
  • Configuration functions for common agent types
  • Documentation explaining composition benefits

Try With AI

How would you combine reasoning, database, search, and API capabilities in 15 different agent configurations without creating 15 inheritance classes?

🔍 Explore Composition Patterns:

"Show me how Agent class composes ReasoningEngine, DatabaseEngine, and SearchEngine as capabilities. Explain why this creates 2^3 combinations without exponential class growth."

🎯 Practice Module Organization:

"Design a project structure with agents/, engines/, config/, and utils/ packages. Show init.py files that create clean public APIs. Explain dependency flow."

🧪 Test Capability Injection:

"Write Agent.init that accepts Dict[str, Engine] for flexible capability injection. Show configurations for ChatAgent (reasoning only), AnalystAgent (database only), and UniversalAgent (all capabilities)."

🚀 Apply to Multi-Agent Systems:

"Create a configuration factory that generates 10 agent types by mixing 5 engine types. Demonstrate that adding a new engine (FileEngine) works with all existing agents without code changes."