Polymorphism in Python: Method Overriding and Duck Typing
Polymorphism, derived from Greek meaning "many forms," is a cornerstone of object-oriented programming. It allows you to write code that works with multiple object types without explicitly checking their class. In Python, polymorphism is achieved through two primary mechanisms: method overriding (tied to inheritance) and duck typing (Python's dynamic approach). Together, these enable flexible, maintainable code that scales as you add new classes.
Key Takeaways
- Polymorphism enables a single interface (method name) to represent different underlying implementations across different classes
- Method overriding occurs when a subclass provides its own implementation of a parent class method; Python finds the correct method at runtime
- Duck typing is Python's dynamic approach: if an object has the required methods, it can be used regardless of its class or inheritance chain
- Type checking with
isinstance()is usually unnecessary; trust that objects will have the required methods (less defensive, more Pythonic) - Polymorphism reduces code duplication, improves extensibility, and allows new classes to integrate seamlessly without modifying existing code
What Is Polymorphism with Class Inheritance?
When a child class provides its own implementation of a method from the parent class, that's method overriding—a form of polymorphism through inheritance. This allows different subclasses to respond to the same method call in their own unique ways.
Consider animals that all have a speak() method but produce different sounds:
# Polymorphism through inheritance
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
# Base implementation (abstract pattern)
raise NotImplementedError("Subclass must implement speak()")
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
class Duck(Animal):
def speak(self):
return "Quack!"
# Create instances of different animal types
animals = [Dog("Buddy"), Cat("Whiskers"), Duck("Daffy")]
# Polymorphic code: same method call, different behaviors
for animal in animals:
# We don't check "if isinstance(animal, Dog)" or any type checks
# We trust that any Animal has a speak() method
print(f"{animal.name} says: {animal.speak()}")
Output:
Buddy says: Woof!
Whiskers says: Meow!
Daffy says: Quack!
This is polymorphism in action. The for loop uses the same interface—calling animal.speak()—but Python's runtime dispatch (method resolution) calls the correct implementation for each object's class. You can add new animal types (e.g., Lion, Parrot) and the loop works without modification. This extensibility is polymorphism's power.
How Does Method Resolution Work in Python?
When you call animal.speak(), Python uses the Method Resolution Order (MRO) to find which speak() method to execute. It starts with the object's own class, then checks parent classes from left to right. Use the mro() method to inspect the resolution chain:
# Understanding Method Resolution Order
class Animal:
def speak(self):
raise NotImplementedError()
class Dog(Animal):
def speak(self):
return "Woof!"
# Check the MRO
print(Dog.mro())
# Output: [<class 'Dog'>, <class 'Animal'>, <class 'object'>]
# When you call Dog().speak(), Python checks:
# 1. Does Dog have speak()? YES → execute Dog.speak()
dog = Dog()
print(dog.speak()) # Output: Woof!
What Is Duck Typing?
Duck typing is Python's dynamic approach to polymorphism. The name comes from the phrase: "If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck."
In programming terms, an object's type is less important than its methods. If an object has the required methods, it can be used for that task, regardless of its class or inheritance chain. This decouples your code from specific class hierarchies and enables incredible flexibility.
Unlike strict type systems that require objects to inherit from a common base class, Python allows any object with the required methods to work together:
# Duck typing: no common base class required
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
# This class has NO relationship to Dog or Cat
class Car:
def speak(self):
return "Vroom!"
def make_it_speak(some_object):
"""Call .speak() on any object that has this method."""
print(some_object.speak())
# All three objects work because they all have speak()
my_dog = Dog()
my_cat = Cat()
my_car = Car()
make_it_speak(my_dog) # Output: Woof!
make_it_speak(my_cat) # Output: Meow!
make_it_speak(my_car) # Output: Vroom!
The make_it_speak() function doesn't care about types. It only checks: "Does this object have a .speak() method?" Because all three classes have it, they all work. This is the essence of duck typing—code is organized around behavior (methods) rather than type hierarchies.
Inheritance vs. Duck Typing: Which Approach Should You Use?
| Scenario | Approach | Example |
|---|---|---|
| Shared code among classes | Inheritance | Base Animal class with common attributes |
| Enforcing a contract/interface | Inheritance + abstract methods | Require all animals to implement speak() |
| Maximum flexibility | Duck typing | Any object with speak() works |
| Type safety and IDE support | Type hints + inheritance | def handle_animal(a: Animal): |
| Dynamic, script-like code | Duck typing | Process objects by capability, not class |
Best practice: Use inheritance when classes share significant code or represent a real "is-a" relationship (a Dog is an Animal). Use duck typing for loose coupling and flexibility (any thing that can speak() is useful).
Real-World Example: Processing Different Payment Methods
Here's a practical case where polymorphism shines:
# Payment processing with duck typing
class CreditCard:
def charge(self, amount):
return f"Charged ${amount} to credit card"
class PayPal:
def charge(self, amount):
return f"PayPal charge: ${amount}"
class Bitcoin:
def charge(self, amount):
return f"Sent {amount} satoshis"
class ApplePay:
def charge(self, amount):
return f"Apple Pay: ${amount}"
def process_payment(payment_method, amount):
"""Works with any payment method that has charge()."""
print(payment_method.charge(amount))
# All payment methods work with the same function
process_payment(CreditCard(), 50.00) # Output: Charged $50.0 to credit card
process_payment(PayPal(), 50.00) # Output: PayPal charge: $50.0
process_payment(Bitcoin(), 50.00) # Output: Sent 50.0 satoshis
process_payment(ApplePay(), 50.00) # Output: Apple Pay: $50.0
# Add a new payment method later—no changes to process_payment needed!
class GooglePay:
def charge(self, amount):
return f"Google Pay: ${amount}"
process_payment(GooglePay(), 50.00) # Works immediately!
Without polymorphism, process_payment() would require explicit if isinstance() checks for each payment type, making it rigid and hard to extend. With duck typing, new payment methods integrate seamlessly.
Combining Inheritance and Duck Typing
In practice, you often use both. Inheritance provides code reuse and explicit contracts; duck typing provides flexibility:
# Hybrid approach: inheritance for code reuse, duck typing for flexibility
class DatabaseConnection:
"""Abstract base for any database connection."""
def execute(self, query):
raise NotImplementedError()
class PostgreSQL(DatabaseConnection):
def execute(self, query):
return f"PostgreSQL executing: {query}"
class MongoDB(DatabaseConnection):
def execute(self, query):
return f"MongoDB executing: {query}"
# Unrelated class with same interface (duck typing)
class ElasticSearch:
def execute(self, query):
return f"Elasticsearch executing: {query}"
def run_query(connection, sql):
"""Works with any object that has execute()."""
print(connection.execute(sql))
# Inheritance-based classes
pg = PostgreSQL()
mongo = MongoDB()
# Unrelated class (duck typing)
es = ElasticSearch()
run_query(pg, "SELECT * FROM users") # Inheritance-based
run_query(mongo, "db.users.find({})") # Inheritance-based
run_query(es, "GET /index/_search") # Duck-typed (no inheritance)
Frequently Asked Questions
Why is duck typing better than explicit type checking?
Duck typing reduces coupling and improves extensibility. Code like if isinstance(obj, Dog): obj.bark() only works with Dog objects (or subclasses). Code like obj.bark() works with any object that has a bark() method, including future classes you haven't written yet. This flexibility is why Python encourages "duck typing over type checking."
What happens if you call a method that doesn't exist?
Python raises an AttributeError at runtime: AttributeError: 'Car' object has no attribute 'roar'. This is why duck typing relies on protocol documentation—the code should clearly specify what methods are expected. Type hints like def handle(obj: Drawable): help document this, or just write clear docstrings.
Is duck typing safe?
It is as safe as your design. If you document expected methods clearly (in docstrings or type hints), and objects implement them correctly, duck typing is perfectly safe and very Pythonic. Testing also catches mismatches. The tradeoff: you lose compile-time type checking but gain runtime flexibility.
How do you prevent subclasses from overriding critical methods?
Use @final decorator (Python 3.8+) from typing:
from typing import final
class DatabaseBase:
@final
def connect(self):
"""Must not be overridden."""
return self._connect_impl()
def _connect_impl(self):
raise NotImplementedError()
What is the difference between overriding and overloading?
Overriding (Python supports this) means a subclass provides a new implementation of a parent method—same signature, different body. Overloading (Python does not support this) means multiple methods with the same name but different parameters. Python allows default parameters and *args/**kwargs for flexible signatures instead.
Conclusion
Polymorphism is a powerful tool that makes your code flexible, extensible, and maintainable. Method overriding through inheritance provides code reuse and clear hierarchies. Duck typing enables dynamic flexibility without rigid type constraints. Together, they allow you to write generic code that adapts to new types without modification—a key principle of the Open/Closed Principle in software design.
As you design Python systems, think in terms of behavior (what methods an object must have) rather than strict types (what class it is). This mindset, natural to Python, produces code that scales elegantly as requirements evolve.