Python Polymorphism Examples: Duck Typing and Overriding
python polymorphism examples: Practical Python polymorphism examples covering duck typing, method overriding, abstract base classes, and runtime behavior for working d...
When a function calls a method on an object, Python does not care about the object's declared type. It only cares whether the object has the method being called. This runtime behavior is polymorphism in its most practical form. In this article, we'll explore concrete python polymorphism examples that show how duck typing, method overriding, and abstract base classes shape the way you write flexible code.
How Polymorphism Works in Python
Python resolves method calls at runtime by looking up the attribute on the actual object. Consider this minimal example:
class Dog: def speak(self): return "Woof" class Cat: def speak(self): return "Meow" def make_speak(animal): print(animal.speak()) make_speak(Dog()) make_speak(Cat())
Both Dog and Cat have a speak method. The function make_speak does not check the type; it simply calls speak(). Python's dynamic dispatch handles the rest. This is the simplest form of polymorphism: the same interface, different implementations, selected at runtime.
Duck Typing: The Core of Python Polymorphism
The phrase "if it walks like a duck and quacks like a duck, then it must be a duck" captures Python's approach. You do not need inheritance to achieve polymorphism. Any object that provides the expected method can be used interchangeably. This gives you enormous flexibility but also shifts responsibility to the caller.
class Car: def start(self): return "Engine started" class Bicycle: def start(self): return "Pedaling" def begin_trip(vehicle): print(vehicle.start()) begin_trip(Car()) begin_trip(Bicycle())
Here, Car and Bicycle are unrelated classes. The function begin_trip works with both because they both expose start. Duck typing means you design your code around behavior, not type hierarchies. This is especially useful when integrating third-party objects or when you want to avoid forcing a common base class.
Method Overriding in Inheritance Hierarchies
When you do use inheritance, polymorphism appears through method overriding. A subclass can replace a method inherited from its parent, and calls through the parent type will invoke the subclass's version.
class Shape: def area(self): raise NotImplementedError class Square(Shape): def __init__(self, side): self.side = side def area(self): return self.side ** 2 class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14159 * self.radius ** 2 def print_area(shape): print(f"Area: {shape.area()}") print_area(Square(4)) print_area(Circle(3))
The base class Shape defines an area method that raises NotImplementedError. Subclasses override it. When print_area calls shape.area(), Python dispatches to the actual subclass's implementation. This is classic polymorphism through inheritance, but note that the base class is not strictly required for the function to work—duck typing would also suffice.
Abstract Base Classes for Explicit Polymorphic Interfaces
If you want to enforce that certain methods exist, you can use Python's abc module. Abstract base classes (ABCs) let you define an interface and mark methods as abstract. Subclasses must implement them, or they cannot be instantiated.
from abc import ABC, abstractmethod class Payment(ABC): @abstractmethod def pay(self, amount): pass class CreditCard(Payment): def pay(self, amount): return f"Paid {amount} with credit card" class PayPal(Payment): def pay(self, amount): return f"Paid {amount} with PayPal" def checkout(payment_method, amount): print(payment_method.pay(amount)) checkout(CreditCard(), 100) checkout(PayPal(), 200)
Here, Payment is an ABC. Any class that inherits from it must implement pay. This gives you compile-time-like guarantees (though still at runtime) that all payment methods support the same interface. ABCs are useful when you control the class hierarchy and want to enforce a contract across multiple implementations.
Polymorphism with Functions and Higher-Order Functions
Polymorphism is not limited to objects. Functions in Python are first-class citizens, so you can pass them around and call them generically. This is a form of polymorphism where the behavior is the function itself.
def add(a, b): return a + b def multiply(a, b): return a * b def apply_operation(operation, x, y): return operation(x, y) print(apply_operation(add, 3, 4)) print(apply_operation(multiply, 3, 4))
The apply_operation function accepts any callable that takes two arguments. This is polymorphic because the operation can vary at runtime. It is a common pattern in data processing and functional programming, and it works because Python treats functions as objects.
Runtime Behavior: isinstance, issubclass, and Type Checks
While duck typing is flexible, sometimes you need to know an object's type. Python provides isinstance and issubclass for runtime type checks. These are often used to handle different types differently, but they can also break polymorphism if overused.
class Animal: pass class Dog(Animal): pass class Cat(Animal): pass def describe(animal): if isinstance(animal, Dog): return "A dog" elif isinstance(animal, Cat): return "A cat" else: return "Unknown animal"
This approach ties your function to specific classes, reducing flexibility. In most cases, relying on duck typing is cleaner. However, isinstance is valuable when you need to handle objects from different libraries that do not share a common interface, or when you need to distinguish between types for serialization or logging.
Maintainability and Compatibility Considerations
Polymorphism in Python is powerful but requires discipline. Duck typing makes code flexible but can hide missing methods until runtime. If you call a method that does not exist, you get an AttributeError only when that line executes. This can be difficult to debug in large codebases.
Using ABCs adds clarity and early failure. If a class is missing an abstract method, you get a TypeError at instantiation time, which is much earlier than a call site error. This makes ABCs a good choice for public APIs or frameworks where you want to enforce a contract.
Another consideration is compatibility. Python's dynamic nature means that polymorphism works across Python 3 versions without issue, but be careful with @abstractmethod and super() calls when mixing old-style and new-style classes. In Python 3, all classes are new-style, so this is rarely a problem. Still, when you override a method, you should decide whether to call super().method() to preserve parent behavior. Failing to do so can break initialization logic.
Finally, remember that polymorphism is a tool, not a goal. Overusing inheritance hierarchies can make code rigid. Duck typing and ABCs both have their place. Choose the approach that matches the stability of your interfaces and the level of control you need. For internal code with few callers, duck typing is often sufficient. For public APIs, ABCs provide a clearer contract and better error messages.