Polymorphism — Duck Typing and Interfaces
Table of Contents
- Introduction to Polymorphism
- Basic Polymorphism Example
- Duck Typing in Python
- Advantages of Duck Typing
- Interfaces in Python (Implicit Interfaces)
- When to Use Stricter Interfaces
- Abstract Base Classes (ABCs)
- Purpose of ABCs
- Creating an ABC with abc Module
- Multiple Abstract Methods Example
- Partial Implementations in ABCs
- Operator Overloading and Magic Methods
- Iterators and Generators in Custom Classes
- Type Hints and Structural Typing (Protocols)
- Common OOP Pitfalls
- OOP Best Practices
- Complete Example — Library System Design
- When (and When Not) to Use OOP
Polymorphism (Greek: poly = many, morph = form) means “many forms”.
In programming, it allows the same operation or function name to behave differently depending on the object.
It enables writing flexible and reusable code because different objects can respond to the same function or method in their own unique way.
Example of Polymorphism
class Dog:
def speak(self):
return "Bark"
class Cat:
def speak(self):
return "Meow"
# Same function name behaves differently
animals = [Dog(), Cat()]
for animal in animals:
print(animal.speak())
Output:
Bark
Meow
✅ Explanation:
- Both Dog and Cat have the same method name speak().
- But each implements it differently.
- The loop can call speak() on any object, without caring about its type — that’s polymorphism.
Duck Typing
Python follows a dynamic typing philosophy, often summarized by:
“If it looks like a duck and quacks like a duck, it’s a duck.”
This means:
- Python doesn’t require objects to belong to a specific class or implement an interface explicitly.
- If an object has the required behavior (methods/attributes), it can be used — regardless of its type.
Example of Duck Typing
class Duck:
def quack(self):
print("Quack! Quack!")
class Person:
def quack(self):
print("I’m imitating a duck!")
def make_it_quack(thing):
thing.quack() # Only expects the object to have 'quack' method
make_it_quack(Duck())
make_it_quack(Person())
Output:
Quack! Quack!
I’m imitating a duck!
✅ Explanation:
- make_it_quack() doesn’t check whether the object is a Duck or a Person.
- It simply calls thing.quack().
- Both objects “quack”, so it works perfectly — that’s duck typing.
Advantages of Duck Typing
✅ Flexibility – You can use any object that implements the right behavior.
✅ Less boilerplate – No need for explicit inheritance or interface implementation.
✅ Dynamic nature – Encourages simpler, more readable code.
Interfaces in Python
Unlike Java or C++, Python does not have explicit interfaces as part of the language.
Instead, “interfaces” are defined implicitly — if an object supports the required methods, it fits the interface.
Example:
- A “file-like object” in Python is anything that has .read() and .write() methods — not necessarily an actual file.
Stricter (Formal) Interfaces in Python — What Are They?
Stricter (Formal) Interfaces in Python are explicitly defined contracts that specify which methods and properties a class must implement, enforced by the language rather than by convention.
In Python, these are implemented using Abstract Base Classes (ABCs). An ABC defines abstract methods that must be implemented by any subclass, otherwise the subclass cannot be instantiated. This enforcement happens at class creation/instantiation time, not at runtime usage.
This is where Abstract Base Classes (ABCs) come in.
When You Need Stricter / Formal Interfaces
Sometimes, duck typing is too loose:
- You want to guarantee certain methods exist
- You want errors to appear at class creation time, not runtime
- You want clearer API contracts
In these cases, Python provides Abstract Base Classes (ABCs).
10. Abstract Base Classes (ABCs)
An Abstract Base Class (ABC) is a special class that acts as a blueprint for other classes.
It defines a common interface that all its subclasses must follow.
You cannot create objects (instances) of an abstract class directly —
it only defines what methods must exist, not how they are implemented.
Purpose of ABCs
✅ Enforce method implementation in subclasses.
✅ Define contracts for a group of related classes.
✅ Make code more structured and error-free.
Using abc Module
Python provides the abc (Abstract Base Classes) module for this purpose.
You use:
- ABC as the base class for abstract classes
- @abstractmethod decorator to mark required methods
Example:
from abc import ABC, abstractmethod
class Shape(ABC): # Abstract Base Class
@abstractmethod
def area(self):
pass
class Circle(Shape): # Concrete subclass
def __init__(self, r):
self.r = r
def area(self):
return 3.14 * self.r * self.r
# s = Shape() # ❌ Error: Can't instantiate abstract class
c = Circle(5)
print(c.area())
Output:
78.5
✅ Explanation:
- Shape defines an abstract method area().
- Any class inheriting from Shape must implement area().
- Attempting to instantiate Shape directly raises an error.
Benefits of Abstract Base Classes
✅ Provides structure – ensures subclasses follow a defined pattern.
✅ Acts as documentation for expected methods.
✅ Helps with type checking and consistency in large projects.
Example: Multiple Abstract Methods
from abc import ABC, abstractmethod
class Payment(ABC):
@abstractmethod
def pay(self, amount): pass
@abstractmethod
def refund(self, amount): pass
class CreditCardPayment(Payment):
def pay(self, amount):
print(f"Paid {amount} using Credit Card")
def refund(self, amount):
print(f"Refunded {amount} to Credit Card")
c = CreditCardPayment()
c.pay(1000)
c.refund(500)
Output:
Paid 1000 using Credit Card
Refunded 500 to Credit Card
✅ Explanation:
- Payment defines two required methods: pay() and refund().
- Any subclass of Payment must implement both.
Partial Implementation
A subclass can implement some abstract methods and remain abstract itself.
It becomes instantiable only when all abstract methods are defined.
11. Operator overloading and magic methods
Python special (dunder) methods let objects behave like builtins:
- __str__/__repr__ — string representations
- __add__/__sub__/__mul__ — arithmetic operators
- __lt__/__eq__/__hash__ — comparison and hashing
- __len__/__iter__/__next__ — container and iterator behavior
- __call__ — make instances callable
Example:
class Vector:
def __init__(self, x,y): self.x,self.y = x,y
def __add__(self, other): return Vector(self.x+other.x, self.y+other.y)
def __repr__(self): return f"Vector({self.x},{self.y})"
Now v1 + v2 uses __add__. Implementing __eq__ and __hash__ carefully matters for using objects as dict keys or in sets.
12. Iterators and generators in classes
To make an object iterable, implement __iter__() returning an iterator (object with __next__()), or make __iter__ a generator.
class Counter:
def __init__(self, n): self.n = n
def __iter__(self):
for i in range(self.n):
yield i
This allows for x in Counter(3):.
13. Composition vs Inheritance (prefer composition often)
- Inheritance says “is-a”: Car is a Vehicle.
- Composition says “has-a”: Car has an Engine.
Composition often yields looser coupling and better flexibility; prefer composition for reusing behavior rather than creating deep inheritance hierarchies.
Example composition:
class Engine:
def start(self): print("engine started")
class Car:
def __init__(self): self.engine = Engine()
def drive(self):
self.engine.start()
print("car driving")
14. Common pitfalls and best practices
Pitfalls
- Mutable class attributes leading to shared state bugs.
- Overusing inheritance (deep hierarchies increase complexity).
- Catching exceptions too broadly hides bugs.
- Relying on __del__ for resource management.
- Not implementing __eq__ & __hash__ carefully when objects are used in sets/dicts.
Best practices
- Keep classes small and focused (Single Responsibility).
- Prefer composition over inheritance when possible.
- Use properties for controlled attribute access.
- Keep __init__ simple (avoid heavy work); use factory methods for complex creation.
- Document public API of classes and methods.
- Write unit tests for object behavior, especially edge cases.
- Use dataclasses for simple data containers.
- Use abc for clear abstract interfaces when needed.
15. Example: complete small OOP design — Library system
A small sample tying many concepts together (encapsulation, inheritance, abstract base class, composition):
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import date
@dataclass
class Person:
name: str
class Item(ABC):
def __init__(self, title: str):
self.title = title
self._borrowed_by = None
def is_available(self):
return self._borrowed_by is None
def borrow(self, member: 'Member'):
if not self.is_available():
raise ValueError("Item not available")
self._borrowed_by = member
self._borrow_date = date.today()
def return_item(self):
self._borrowed_by = None
self._borrow_date = None
@abstractmethod
def borrow_period(self) -> int: pass
class Book(Item):
def borrow_period(self) -> int:
return 21 # days
class DVD(Item):
def borrow_period(self) -> int:
return 7
@dataclass
class Member(Person):
member_id: int
borrowed: list = field(default_factory=list)
def borrow_item(self, item: Item):
item.borrow(self)
self.borrowed.append(item)
def return_item(self, item: Item):
item.return_item()
self.borrowed.remove(item)
This design shows:
- Item is abstract (requires borrow_period).
- Member composes Item usage.
- Encapsulation: _borrowed_by is internal (single underscore).
- dataclass for simple data structs.
Patterns and when to use OOP
OOP isn't the only way to organize code — functional or procedural approaches often fit. Use OOP when:
- You model domain objects with state and behavior.
- You need to group related behavior and maintain invariants.
- You want polymorphism (different implementations under same interface).
- You need to model complex mutable state over time.
Design patterns (Factory, Strategy, Adapter, Observer) are often implemented with classes — but remember patterns are solutions to recurring design problems, not absolute requirements.
Summary
Polymorphism allows the same operation to behave differently depending on the object, enabling flexible, reusable, and extensible code. In Python, polymorphism is most commonly achieved through duck typing, where behavior matters more than an object’s concrete type.
Python’s dynamic nature encourages implicit interfaces — if an object implements the required methods, it can be used without formal inheritance. This makes code simpler and more adaptable, but also places responsibility on developers to follow conventions carefully.
When greater reliability and clarity are needed, Python provides Abstract Base Classes (ABCs) to define stricter, formal interfaces. ABCs act as enforceable contracts that guarantee required method implementation, helping catch errors early and improving maintainability in larger systems.
Beyond interfaces, Python supports rich polymorphic behavior through operator overloading, magic methods, iterators, and generators, allowing custom objects to integrate seamlessly with built-in language features. Thoughtful use of composition over inheritance, combined with clear abstractions, leads to more flexible and maintainable designs.
Finally, effective object-oriented programming in Python is about balance. OOP is most powerful when modeling real-world entities with shared behavior, but it should be applied judiciously. Understanding when to use duck typing, when to enforce interfaces, and when to avoid OOP altogether is key to writing clean, scalable Python code.
