Polymorphism
One call, many behaviours — code that talks to an interface works with every class that implements it, including ones written later.
Big idea
Polymorphism lets you write a call once — shape.area() — and have each object answer in its own way. The caller depends on what objects can do, not which class they are, so new kinds can be added without touching the caller. It's the engine behind almost every pattern in this track.
The problem it solves
Without it, code that handles several kinds of thing ends up asking each one what it is:
def area(shape):
if shape["kind"] == "circle":
return 3.14159 * shape["r"] ** 2
elif shape["kind"] == "rect":
return shape["w"] * shape["h"]
# add a triangle? edit this function — and every other one like it
The same if kind == … ladder shows up in area, perimeter, draw, to_json. Adding a shape means hunting down every ladder; missing one is a bug that only shows up at runtime.
Think of it like
A wall socket. The socket doesn't know or care whether you plug in a kettle, a lamp, or a laptop charger — it offers the same two holes and the same voltage, and each device does its own thing with it. New devices get invented every year and the socket never changes.
The kinds you'll meet
- Subtype polymorphism (the one this track means by default) — subclasses or implementers of an interface override a method; the call is dispatched to the right one at runtime.
- Duck typing — Python's flavour: anything with an
area()method works, no shared base class required. "If it quacks like a duck…" - Overloading — same method name, different parameter lists, picked at compile time (Java, C++). Python fakes it with default arguments.
- Parametric polymorphism — generics:
list[T]works the same for anyT.
In code
import math
class Shape:
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return math.pi * self.r ** 2
class Rect(Shape):
def __init__(self, w, h): self.w, self.h = w, h
def area(self): return self.w * self.h
class Triangle(Shape): # added later — total() didn't change
def __init__(self, b, h): self.b, self.h = b, h
def area(self): return 0.5 * self.b * self.h
def total(shapes):
return sum(s.area() for s in shapes) # dispatches on each object's class
shapes = [Circle(1), Rect(2, 3), Triangle(4, 5)]
for s in shapes:
print(f"{type(s).__name__:8} {s.area():.2f}")
print(f"total {total(shapes):.2f}")
class Label: # duck typing: not a Shape, but it has area()
def area(self): return 0.0
print(f"with a label: {total(shapes + [Label()]):.2f}")output
Circle 3.14
Rect 6.00
Triangle 10.00
total 19.14
with a label: 19.14Run and edit it in the app.
With polymorphic shapes, what do you change to add a Hexagon?
Dispatch, step by step
When Python evaluates s.area(), it looks for area on the object's actual class first, then up through its parents. The variable s has no fixed type; the object decides. In Java the variable might be declared Shape s, but the JVM still calls the override of the object's runtime class — that's dynamic dispatch.
class Animal:
def speak(self):
return "..."
def greet(self):
return "I say " + self.speak()
class Dog(Animal):
def speak(self):
return "woof"
pets = [Animal(), Dog()]
for p in pets:
print(p.greet())What does it print? Show the answer
I say ...
I say woofRun and edit it in the app.
greet lives only in Animal, but self.speak() inside it still dispatches to Dog.speak when self is a dog.
Watch it
When to use it — and when not to
Use it whenever you see the same switch on type in more than one place, or when you expect new variants to keep arriving (payment methods, file formats, notification channels).
Don't force it when the set of cases is small, closed, and only switched on once — a single match on three enum values is simpler than three classes. And if what varies is data (a tax rate per country), use a lookup table, not a class per country.
Pros and cons
| Pros | Cons |
|---|---|
| New variants without editing callers (Open/Closed) | Behaviour is spread across many classes |
| Removes repeated type switches | "Which area() actually runs?" takes a jump-to-definition |
| Makes test doubles trivial | Adding a new operation means touching every class (see Visitor) |
How it relates
- Interfaces and Abstract Classes name the contract that polymorphic callers rely on.
- Strategy, State, Command are polymorphism with a purpose.
- The refactoring Replace Conditional with Polymorphism (Simplifying Conditionals) turns the
if kind ==ladder into this.
In Python, which objects can be passed to total(shapes), which calls s.area() on each?
Key terms
- Polymorphism — Treating different classes through one interface, with each supplying its own behaviour.
- Dynamic dispatch — Choosing which method implementation to run based on the object's runtime class.
- Override — A subclass's own version of a method defined by its parent.
- Duck typing — Accepting any object that has the needed methods, regardless of its class.