
Python Course
Polymorphism in Python
The word polymorphism comes from Greek — "many forms." In Python, it describes the ability of different object types to respond to the same method call, each in their own way. You saw this in the Inheritance lesson when different animal subclasses had their own speak() method. This lesson goes much deeper — covering every form of polymorphism Python supports, including duck typing, operator overloading, and the design principles that make polymorphism so valuable.
Polymorphism is not a feature you bolt on — it is a natural consequence of good object design. Once you understand it, you will start seeing and using it everywhere.
The Core Idea
In Python, polymorphism means you can write code that works on objects of different types — as long as those objects support the operations you are calling. You do not need to check what type something is. You just call the method and trust each object to do the right thing.
class Dog:
def speak(self): return "Woof!"
class Cat:
def speak(self): return "Meow!"
class Duck:
def speak(self): return "Quack!"
# A single function works with any of these — no isinstance() checks
def make_noise(animal):
print(animal.speak())
for creature in [Dog(), Cat(), Duck(), Dog()]:
make_noise(creature)
# Adding a new type requires zero changes to make_noise()
class Parrot:
def speak(self): return "Polly wants a cracker!"
make_noise(Parrot())make_noise()does not care about the type — it only cares that the object has aspeak()method.- Adding a new type requires no changes to any existing code — this is the open/closed principle: open for extension, closed for modification.
1. Polymorphism Through Inheritance
The most explicit form: override a parent method in each subclass. A shared interface is defined once in the parent and each child implements it differently.
import math
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement area()")
def perimeter(self):
raise NotImplementedError("Subclasses must implement perimeter()")
def describe(self):
print(f"{self.__class__.__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}")
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Rectangle(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
def perimeter(self):
return 2 * (self.w + self.h)
class Triangle(Shape):
def __init__(self, a, b, c):
self.a, self.b, self.c = a, b, c
def area(self):
s = (self.a + self.b + self.c) / 2
return math.sqrt(s * (s-self.a) * (s-self.b) * (s-self.c))
def perimeter(self):
return self.a + self.b + self.c
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 4, 5)]
for s in shapes:
s.describe()
total_area = sum(s.area() for s in shapes)
print(f"\nTotal area: {total_area:.2f}")describe()is defined once and callsself.area()andself.perimeter()— Python dispatches to the correct subclass version at runtime.- Adding a
Pentagonclass requires only writing the new class — the loop,sum(), anddescribe()all work unchanged.
2. Duck Typing — Python's Natural Polymorphism
Python does not require a shared parent class for polymorphism. If an object has the method you need, you can call it — regardless of type. This is duck typing: "if it walks like a duck and quacks like a duck, it is a duck."
import json
# No shared parent — just the same interface (export method)
class PDFExporter:
def export(self, data):
print(f"[PDF] Writing {len(data)} records to report.pdf")
class CSVExporter:
def export(self, data):
headers = ",".join(data[0].keys())
rows = len(data)
print(f"[CSV] {headers} — {rows} rows written to report.csv")
class JSONExporter:
def export(self, data):
print(f"[JSON] {json.dumps(data[:1])}... ({len(data)} records)")
# This function works with any exporter — duck typing
def run_export(exporter, data):
exporter.export(data)
records = [
{"name": "Alice", "score": 92},
{"name": "Bob", "score": 88},
{"name": "Carol", "score": 95},
]
for exporter in [PDFExporter(), CSVExporter(), JSONExporter()]:
run_export(exporter, records)- None of the exporter classes share a parent — duck typing does not require it.
- If an object does not have the required method, Python raises
AttributeErrorat call time — not before. - Duck typing is why Python code is often more concise and composable than equivalent code in statically typed languages.
3. Operator Polymorphism — Built-In Operators
Python's operators are polymorphic by design — the same symbol does different things depending on the type. This works because each type implements the underlying dunder method in its own way.
# Same operators — different types, different behaviour
print(10 + 5) # 15 — integer addition
print(10.5 + 5) # 15.5 — float addition
print("Hello" + " World") # Hello World — string concatenation
print([1, 2] + [3, 4]) # [1, 2, 3, 4] — list merge
print(len("Python")) # 6 — string length via __len__
print(len([1, 2, 3])) # 3 — list length via __len__
print(len({"a": 1, "b": 2})) # 2 — dict length via __len__
print("ha" * 3) # hahaha — string repeat
print([0] * 5) # [0,0,0,0,0] — list repeat
# Your own class can join this via dunder methods
class Money:
def __init__(self, amount, currency="USD"):
self.amount = amount
self.currency = currency
def __add__(self, other):
if self.currency != other.currency:
raise ValueError("Currency mismatch")
return Money(self.amount + other.amount, self.currency)
def __repr__(self):
return f"Money({self.amount}, {self.currency!r})"
a = Money(10); b = Money(25)
print(a + b) # Money(35, 'USD') — same + operator, custom type4. Polymorphism with Built-In Functions
Python's built-in functions like str(), len(), iter(), sorted(), and repr() are all polymorphic — they work on any object that implements the corresponding dunder method.
class Product:
def __init__(self, name, price, stock):
self.name = name
self.price = price
self.stock = stock
def __str__(self): return f"{self.name} — ${self.price:.2f}"
def __repr__(self): return f"Product({self.name!r}, {self.price})"
def __len__(self): return self.stock
def __bool__(self): return self.stock > 0
def __lt__(self, other): return self.price < other.price
items = [
Product("Desk", 89.99, 5),
Product("Notebook", 4.99, 0),
Product("Pen", 1.50, 50),
Product("Lamp", 24.99, 12),
]
# str() — polymorphic via __str__
for p in items:
print(str(p))
# bool() — polymorphic via __bool__
in_stock = [p for p in items if p] # uses __bool__
print(f"\nIn stock ({len(in_stock)}):", [p.name for p in in_stock])
# sorted() — polymorphic via __lt__
by_price = sorted(items)
print("By price:", [p.name for p in by_price])5. isinstance() vs type() — When Type Checking is Needed
Pure duck typing is ideal, but occasionally you need to handle different types differently. In those cases, always prefer isinstance() over type() ==.
class Animal:
def __init__(self, name): self.name = name
class Dog(Animal):
def speak(self): return "Woof!"
class GuideDog(Dog): # subclass of Dog
pass
class Cat(Animal):
def speak(self): return "Meow!"
def check_type(animal):
if isinstance(animal, Dog): # True for Dog AND GuideDog
print(f"{animal.name}: is a Dog or subclass")
elif isinstance(animal, Cat):
print(f"{animal.name}: is a Cat")
check_type(Dog("Rex"))
check_type(GuideDog("Max")) # correctly identified as Dog
check_type(Cat("Luna"))
# isinstance() vs type() — critical difference
buddy = GuideDog("Buddy")
print(isinstance(buddy, Dog)) # True — respects inheritance
print(type(buddy) == Dog) # False — too strict, misses subclassisinstance(obj, Class)returnsTruefor the class and all its subclasses — correct for polymorphic code.type(obj) == ClassreturnsTrueonly for the exact class — misses subclasses and breaks polymorphism.- Prefer duck typing over
isinstance()checks — only useisinstance()when you genuinely need to branch based on type.
Quick Reference Table
| Form | How It Works | Key Idea |
|---|---|---|
| Inheritance-based | Subclasses override a shared parent method | Runtime dispatch to correct subclass version |
| Duck typing | Any object with the required method works | No shared parent needed — just the right interface |
| Operator overloading | Same operator behaves differently per type | Powered by dunder methods like __add__, __len__ |
| Built-in functions | str(), len(), sorted() work on any compatible type | Each type implements __str__, __len__, __lt__ etc. |
isinstance() | Type-aware branching that respects inheritance | Use sparingly — prefer duck typing |
Practice
What does polymorphism mean in Python?
What is duck typing?
Why is isinstance() preferred over type() == when type checking is needed?
What design principle does polymorphism support — open for extension, closed for modification?
What happens if you call a method on an object that does not have it in duck-typed code?
Which dunder method enables sorted() to work on your custom objects?
Quick Quiz
Which best describes polymorphism?
In duck typing, what is the only requirement for an object to be used in a function?
Why does len() work equally on strings, lists, dicts, and custom classes?
When a parent class raises NotImplementedError in a method, what is it communicating?
Which of the following demonstrates duck typing correctly?
Does isinstance(GuideDog("Max"), Dog) return True when GuideDog is a subclass of Dog?
@property decorator — the professional way to protect object state.