Skip to main content

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:

Loading Python environment...

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):

Loading Python environment...

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):

Loading Python environment...

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.

Loading Python environment...

Aggregation: Weak Relationship

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

Loading Python environment...

🚀 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:

Loading Python environment...

In another file, import and use these classes:

Loading Python environment...

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:

Loading Python environment...

animals/init.py (controls public API):

Loading Python environment...

main.py (uses the package):

Loading Python environment...

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:

Loading Python environment...

Refactored with Composition:

Loading Python environment...

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):

Loading Python environment...

agents/chat_agent.py (uses composition):

Loading Python environment...

agents/init.py (public API):

Loading Python environment...

main.py (orchestration):

Loading Python environment...

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:

Loading Python environment...

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:

Loading Python environment...

agents/agent.py:

Loading Python environment...

agents/init.py:

Loading Python environment...

engines/init.py:

Loading Python environment...

config.py:

Loading Python environment...

main.py:

Loading Python environment...

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."