Polymorphism
Different objects can respond to the same method call in their own way — which means calling code never needs to check what type it's dealing with.
By the end of this lesson, you can
- Explain polymorphism as different objects responding to the same method call, each in its own way
- Write a loop that calls the same method across a list of different subclass instances
- Explain why polymorphism avoids the need for a chain of type-checking conditionals
Why it matters
The inheritance lesson’s Dog and Cat each overrode speak()
differently. Polymorphism is what that enables at the call site: a
loop that calls .speak() on a list of mixed animals never needs to
check which one it’s looking at — each object already knows how to
respond correctly on its own.
Mental model
Polymorphism means the same method call produces different, correct behavior depending on which object it’s called on — without the calling code branching on type at all.
class Animal:
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return "Woof"
class Cat(Animal):
def speak(self):
return "Meow"
animals = [Dog(), Cat(), Animal()]
for animal in animals:
print(animal.speak())Woof
Meow
...The loop body is just animal.speak() — no if statement asks “is
this a Dog?” anywhere. Each object already knows which version of
speak belongs to it.
The alternative, without polymorphism
def speak_without_polymorphism(animal):
if isinstance(animal, Dog):
return "Woof"
elif isinstance(animal, Cat):
return "Meow"
else:
return "..."This produces the same answers today — but every new kind of animal means finding this function and adding another branch. Polymorphism avoids that entirely: each class handles its own case, wherever it’s defined.
Trace it
| Step | Code | What happens |
|---|---|---|
| 1 | animals = [Dog(), Cat(), Animal()] | the loop above prints Woof, Meow, ... |
| 2 | class Bird(Animal): def speak(self): return "Tweet" | a new subclass, defined separately |
| 3 | animals.append(Bird()) | added to the same list |
| 4 | the exact same for loop runs again | prints Woof, Meow, ..., Tweet — no loop changes needed |
Check your understanding
Or reveal the answer without checking
Answer:12.56, then 9
Each shape's own area() runs: Circle(2).area() = 3.14 * 2 * 2 = 12.56, then Square(3).area() = 3 * 3 = 9 — no type-checking needed in the loop itself.
Or reveal the answer without checking
Answer:Forgetting to add a branch for a new type doesn't raise an error; it silently falls through to the wrong answer
A missing branch in an isinstance chain is a logic error, not a crash — it produces a plausible-looking wrong answer instead of alerting anyone that a case was missed.
Practice: warm-up
Trace this loop — write down what each iteration prints.
class Employee:
def bonus(self):
return 0
class Manager(Employee):
def bonus(self):
return 500
class Intern(Employee):
def bonus(self):
return 0
staff = [Manager(), Intern(), Employee()]
for person in staff:
print(person.bonus())
Stuck? Reveal one hint at a time.
Hint 1
Each object in staff calls its own version of bonus() — check which class each one belongs to.
Hint 2
Intern and Employee both happen to return the same value, but for different reasons — Intern overrides it explicitly, Employee never defines anything different.
Reveal the trace
Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.
Manager().bonus() -> 500 (overridden)
Intern().bonus() -> 0 (overridden, explicitly returns 0)
Employee().bonus() -> 0 (Employee's own, un-overridden version)Practice: apply it
This function checks an employee’s type explicitly instead of using polymorphism:
def get_bonus(person):
if isinstance(person, Manager):
return 500
else:
return 0
A new Director class is added, which should receive a bonus of
1000, but nobody remembers to update get_bonus.
Or reveal the answer without checking
Answer:0 — silently wrong, with no error raised at all
Director falls into the else branch, just like any unrecognized type, returning 0 instead of the intended 1000 — a logic error, not a crash, so nothing flags it.
Modification challenge: replace get_bonus with a bonus() method
defined on each class (Employee, Manager, Director), and replace
the call site with person.bonus() — so a new class can never be
forgotten in a type-checking chain, because there’s no chain at all.
Summary
- Polymorphism means different objects respond to the same method call correctly, each in its own way, without the caller checking type.
- The alternative — a chain of
isinstancechecks — has to be found and updated for every new type, and a missed update fails silently rather than raising an error. - Adding a new subclass that defines the shared method works automatically everywhere that method is already called — no existing code needs to change.
- Polymorphism is inheritance’s practical payoff: shared method names across a family of classes, each handled correctly without type-checking.