Polymorphism and Duck Typing
Polymorphism is the power of object-oriented programming. After mastering inheritance in Lesson 1, you now face a critical design question: Should objects be related through inheritance, or should they simply share a common interface? This lesson teaches you both paths—and when to use each one.
In professional AI systems, polymorphism enables multi-agent architectures where different agent types (ChatAgent, CodeAgent, DataAgent) respond to the same process() call with specialized behavior. Understanding polymorphism and duck typing is essential for building flexible, maintainable systems.
What you'll learn: Method overriding, Abstract Base Classes (deep dive), duck typing philosophy, and the critical design trade-offs between enforcing contracts through ABC versus relying on shared behavior through duck typing.
Method Overriding: Replacing Parent Behavior
Method overriding is the foundation of polymorphism. A subclass provides its own implementation of a parent method, replacing the parent's version entirely. The key insight: the same method call produces different behavior depending on the object's actual type.
Let's start with shapes:
Loading Python environment...
Key insight: Notice that shapes is a list of Shape objects, but each shape.area() call invokes the correct implementation based on the actual object type. Python looks up the method in the object's class first, then walks up the inheritance tree if needed. This is polymorphism—the same interface (area()), different implementations.
💬 AI Colearning Prompt
"Explain how Python determines which
area()method to call when we iterate through the shapes list. What's the underlying mechanism that makes polymorphism work?"
This question pushes you from "it works" to "I understand why it works"—method resolution order determines which version of a method gets called.
Abstract Base Classes: Enforcing Contracts
Now we reach a critical decision point. The Shape class above has a default area() that returns 0.0—but that's misleading. A Shape should have an area, and subclasses must implement it. Python's Abstract Base Classes (ABC) enforce this at the class level.
With Abstract Base Classes, you can declare that certain methods must be implemented by subclasses. If a subclass forgets to implement an abstract method, Python raises a TypeError at instantiation—catching the error early, not at runtime.
Loading Python environment...
🎓 Expert Insight
In AI-native development, ABCs are critical. You cannot deploy an agent that doesn't implement
process(). Python checks this at instantiation (when you create the object), not at runtime (when you call the method). This shifts errors from "oh no, the production system crashed" to "oops, my agent class isn't complete yet."
💬 AI Colearning Prompt
"Explain the difference between an ABC with
@abstractmethodversus a regular base class with methods thatraise NotImplementedError(). Which approach is better and why?"
The answer teaches you about explicit contract enforcement versus runtime error handling—a fundamental design difference.
Abstract Properties and Class Methods
Abstract Base Classes aren't limited to regular methods. You can also make properties and class methods abstract, forcing subclasses to define specific attributes or factory methods.
Loading Python environment...
Specification → AI Prompt Used → Generated Concepts → Validation
At this point, you understand:
- ✅ Inheritance creates parent-child relationships
- ✅ Method overriding replaces parent implementations
- ✅ ABC enforces that subclasses implement specific methods/properties
- ✅ Abstract properties and class methods enforce richer contracts
Now comes a paradigm shift: What if you don't need inheritance at all?
Duck Typing: The Pythonic Way
Here's a question that separates Python from languages like Java or C++: Does an object need to inherit from a specific class to be polymorphic?
Python's answer: No. If it walks like a duck and quacks like a duck, it's a duck.
This is duck typing—focusing on what an object can do (its interface) rather than what it is (its class). You don't need a common parent class; you just need objects that implement the same methods.
Consider payment processing. Instead of forcing all processors to inherit from a PaymentProcessor base class, why not just require them to have a process_payment() method?
Loading Python environment...
No Payment base class. No @abstractmethod decorators. Just objects that do what we need them to do.
🎓 Expert Insight
Duck typing is the heart of Python's philosophy. It's more flexible than inheritance because you don't need to plan a hierarchy ahead of time. If you need a new processor, just implement
process_payment()and it works. No inheritance chain, no complex base class design—just implement the interface you need.
🚀 CoLearning Challenge
Ask your AI: "Design an abstract
PaymentProcessorbase class with@abstractmethod process_payment(). Then show how the same system works with duck typing (no inheritance). Compare the code complexity and flexibility of each approach."
Expected outcome: You'll see that duck typing requires fewer lines of code and creates looser coupling between classes. But you'll also see that ABC provides explicit contracts (which can be valuable).
When to Use ABC vs Duck Typing
This is where design judgment matters. Both approaches work. Choosing correctly depends on your specific problem.
Use ABC When:
- You need explicit enforcement — You want Python to reject incomplete subclasses at instantiation time
- You're building a framework — Other developers will extend your classes, and you want to enforce their implementations
- You need documentation — The ABC clearly documents what a subclass must implement
- You want static type checking — Tools like
mypyunderstand ABC contracts better than duck typing
Example: Building an agent framework where you want to guarantee every agent has process(), get_status(), and shutdown() methods.
Use Duck Typing When:
- You're writing application code — Not a framework; you control all the implementations
- You need flexibility — New implementations might not fit a predefined interface
- The interface is simple — Just one or two methods (like
read()orprocess()) - You want loose coupling — Objects don't need to know about each other's class hierarchy
Example: Writing a data processing pipeline where readers might be files, URLs, databases, or API endpoints. Each just needs a read() method.
💬 AI Colearning Prompt
"Explain when to use ABC vs duck typing. Give me 3 scenarios for each approach and explain why each is appropriate."
Real-World Example: File Readers
Let's see both approaches solving the same problem:
Loading Python environment...
Which is better? For this simple case, duck typing is simpler and requires less code. For a complex framework with many implementations, ABC provides valuable explicit contracts.
Polymorphic Collections
One of polymorphism's greatest powers: You can store objects of different types in a single collection, as long as they share a common interface.
Loading Python environment...
This pattern is foundational for multi-agent systems. You have a collection of heterogeneous agents that all respond to the same interface. Each agent specializes in its domain, but they can all be treated uniformly.
Challenge: Building a Polymorphic Agent Dispatcher
In this challenge, you'll discover why polymorphism matters, learn how both ABC and duck typing work, test your understanding against AI, and build a production-ready system.
Part 1: Experience Type-Checking Problems in Agent Dispatch
Your Role: System architect identifying design gaps with AI collaboration
Discovery Exercise: Exploring Type-Checking Brittleness in Dispatchers
Imagine you're building a multi-agent dispatcher that routes messages to different agent types. Without polymorphism, you must check types explicitly.
💬 AI CoLearning Prompt - Discovering the Type-Checking Problem
"I'm building an agent dispatcher. I have ChatAgent, CodeAgent, and DataAgent - each with a
process(message)method. Show me a dispatcher function WITHOUT polymorphism that usesisinstance()checks to route messages to the right agent type.Then analyze:
- How many isinstance() checks are needed for 3 agent types?
- If I add ImageAgent and VideoAgent (5 types total), how many checks?
- What happens if I forget to add the isinstance() check for a new agent type?
- Why is this dispatcher fragile and hard to maintain?"
Expected Understanding: AI will show you the explosion of isinstance() checks. You'll understand that each new agent type requires modifying the dispatcher function - tight coupling that violates the Open/Closed Principle.
💬 AI CoLearning Prompt - Understanding the Scaling Nightmare
"In my dispatcher with isinstance() checks:
- 3 agent types = 3 if/elif branches
- Each new agent = 1 new branch in dispatcher
Explain the scaling problem:
- At 20 agent types, how maintainable is this code?
- What happens if I add SearchAgent but forget to update the dispatcher?
- How do I know if my dispatcher is missing agent types?
- Why is this violating the 'Open for extension, closed for modification' principle?"
Expected Understanding: AI will explain that the dispatcher becomes unmaintainable as agent types grow. Each addition requires modifying existing code (high risk of bugs). Missing checks cause silent failures.
💬 AI CoLearning Prompt - Previewing the Polymorphism Solution
"You showed me the type-checking problem. Now preview the polymorphism solution:
- What is polymorphism and how does it eliminate isinstance() checks?
- Show me a BaseAgent abstract class with process() method
- Show me how ChatAgent, CodeAgent inherit and implement process()
- Show me a dispatcher that works with ANY Agent subclass - no isinstance()!
- When I add SearchAgent, does the dispatcher code change at all?
Also explain the duck typing alternative: do I even need BaseAgent?"
Expected Understanding: AI will show you that polymorphism makes the dispatcher simple: agent.process(message) works for any agent type. No isinstance() checks, no dispatcher modifications when adding new agents.
Part 2: Learn Polymorphism and Duck Typing as Solutions
Your Role: Student learning from AI Teacher
AI Teaching Prompt
Ask your AI companion:
"I built an agent dispatcher with type checking. ChatAgent, CodeAgent, DataAgent each have a
process(message)method. The dispatcher usesisinstance()to route messages:Loading Python environment...
This is fragile. If I add ImageAgent, I must modify the dispatcher.
Explain:
- What is polymorphism? How does it eliminate type checking?
- Show me two solutions: ABC with @abstractmethod, and duck typing without inheritance
- In each approach, what happens when I add a new agent type? Must I modify the dispatcher?
- When would you choose ABC vs duck typing for this problem?"
Expected AI Response Summary
AI will explain:
- Polymorphism: Same interface, different implementations—Python calls the right method based on object type
- ABC approach: Inherit from Agent base class with @abstractmethod process(); dispatcher accepts Agent, no type checking
- Duck typing approach: No inheritance; any object with process() method works; dispatcher is even simpler
- Adding new agents: Both approaches allow adding agents WITHOUT modifying the dispatcher
- Design trade-off: ABC enforces contracts explicitly; duck typing gives flexibility
AI will show code like:
Loading Python environment...
Convergence Activity
After AI explains, verify understanding:
"Explain what happens in the ABC approach when I create 100 different agent types. Do I need to modify the dispatcher each time? Walk me through the duck typing approach and explain why it's even more flexible."
Deliverable
Write 1-paragraph summary: "How Polymorphism Eliminates Type Checking" explaining the core insight and trade-offs between ABC and duck typing.
Part 3: Challenge AI with Design Edge Cases
Your Role: Student testing AI's understanding
Challenge Design Scenarios
Ask AI to handle these cases:
Challenge 1: ABC Contract Enforcement
"Show me what happens if I create a ChatAgent that inherits from Agent but forgets to implement process(). Then show what happens if I use duck typing and forget to implement process(). What's different about when these errors are caught?"
Expected learning: AI explains that ABC catches errors at instantiation time; duck typing catches them at call time.
Challenge 2: Adding Capability to All Agents
"I want to add logging to all agents. With ABC, I add a log() method to the Agent base class. All agents inherit it automatically. How would duck typing handle this? What's the problem?"
Expected learning: AI explains that duck typing requires manual updates to each class, showing the trade-off.
Challenge 3: Runtime Behavior Discovery
"I have a duck-typed processor function that accepts any object with process(). I pass in an object that has process() but it crashes. How is this different from ABC, which would have caught the problem earlier?"
Expected learning: AI explains structural vs nominal typing, and when each catches errors.
Deliverable
Document your three challenges, AI's responses, and analysis of when each approach (ABC vs duck typing) is appropriate.
Part 4: Build Polymorphic Agent Dispatcher for Production
Your Role: Knowledge synthesizer creating reusable code
Your Agent Dispatcher System
Create agent_dispatcher.py with both approaches, demonstrating when each is appropriate:
Loading Python environment...
Your task: Expand this system with:
- Add 2-3 more specialized agent types (SearchAgent, ResearchAgent)
- Create a dispatcher registry that tracks all agent types
- Add a comparison document:
abc_vs_duck_typing.mdexplaining when to use each - Show that adding new agent types requires NO changes to dispatcher code
Validation Checklist
- ✅ ABC approach works with inherited agents
- ✅ Duck typing approach works with unrelated classes
- ✅ Protocol approach provides static type safety
- ✅ Dispatcher code never uses isinstance() or type checks
- ✅ Adding new agent types requires changing only the agent class
- ✅ All three approaches produce identical behavior
Deliverable
Complete agent_dispatcher.py with all three polymorphic approaches and documentation explaining:
- How polymorphism eliminates type checking
- When ABC provides value (contracts, inheritance of shared methods)
- When duck typing wins (flexibility, loose coupling)
- When Protocol is useful (static type checking without inheritance)
Try With AI
Why does a dispatcher with 50 isinstance() checks become unmaintainable?
🔍 Explore Polymorphic Dispatch:
"Show me how process() works identically across ChatAgent, CodeAgent, and SearchAgent without isinstance checks. Explain why the dispatcher doesn't need to know agent types."
🎯 Practice ABC vs Duck Typing:
"Compare ABC inheritance and duck typing for ImageAgent and VideoAgent. When does compile-time validation catch errors that duck typing misses? Show me both approaches."
🧪 Test Protocol Type Safety:
"Create a typing.Protocol for Processable agents. Show how mypy detects when an agent is missing process() before runtime. Compare this safety to duck typing."
🚀 Apply to Multi-Agent Routing:
"Design a router that dispatches to 20+ agent types based on message content. Use polymorphism to avoid any isinstance() or type() checks. Explain how adding a new agent requires zero router changes."