Object-Oriented Programming~20 min

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.

PythonOne loop, three different behaviors
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())
Output
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

PythonType-checking instead of 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

Adding Bird later, with zero changes to the existing loop
StepCodeWhat happens
1animals = [Dog(), Cat(), Animal()]the loop above prints Woof, Meow, ...
2class Bird(Animal): def speak(self): return "Tweet"a new subclass, defined separately
3animals.append(Bird())added to the same list
4the exact same for loop runs againprints Woof, Meow, ..., Tweet — no loop changes needed

Check your understanding

What prints? class Shape: def area(self): return 0 class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14 * self.radius * self.radius class Square(Shape): def __init__(self, side): self.side = side def area(self): return self.side * self.side shapes = [Circle(2), Square(3)] for shape in shapes: print(shape.area())
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.

Why is a growing isinstance chain risky compared to polymorphism?
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.

  1. Hint 1

    Each object in staff calls its own version of bonus() — check which class each one belongs to.

  2. 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.

What does get_bonus(Director()) return, and is that caught as an error?
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 isinstance checks — 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.

Key terms