Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 9 of 11

Polymorphism means "many forms": the same method call does the right thing for whatever object it lands on. You've already built it — every class that overrides speak() is being polymorphic.

class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

class Cow:
    def speak(self):
        return "Moo!"

for animal in [Dog(), Cat(), Cow()]:
    print(animal.speak())

The loop doesn't know or care which class it has. It just calls speak() and each object answers in its own way.

Why it matters

Without polymorphism, that loop becomes a growing chain of type checks:

class Dog:
    pass

class Cat:
    pass

def speak(animal):
    if isinstance(animal, Dog):
        return "Woof!"
    elif isinstance(animal, Cat):
        return "Meow!"
    return "..."

print(speak(Dog()))

Every new animal means editing that function. With polymorphism, a new animal is a new class — and the loop that prints them never changes. The behaviour lives with the thing it describes.

Duck typing

Notice the first example had no shared parent class. Python doesn't require one: if an object has a speak() method, you can call it. This is duck typing — "if it walks like a duck and quacks like a duck, treat it as a duck."

class Robot:
    def speak(self):
        return "Beep boop."

class Dog:
    def speak(self):
        return "Woof!"

for thing in [Dog(), Robot()]:
    print(thing.speak())

A robot is not an animal, but it can still take part — it has the method the loop needs. Python cares about what an object can do, not what it is.

Inheritance is still useful when the classes genuinely share code. Duck typing means you don't need it just to get polymorphism.

Polymorphism with built-ins

The built-in functions work the same way. len() calls __len__, str() calls __str__, + calls __add__ — the same call, different behaviour per type:

print(len("hello"))
print(len([1, 2, 3]))
print(len({"a": 1}))

print(1 + 2)
print("a" + "b")
print([1] + [2])

The next lesson shows how to plug your own classes into these.

A practical example

class Shape:
    def area(self):
        return 0

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Square(Shape):
    def __init__(self, side):
        self.side = side

    def area(self):
        return self.side ** 2

shapes = [Rectangle(3, 4), Square(5), Rectangle(2, 2)]

for shape in shapes:
    print(shape.area())

print(sum([s.area() for s in shapes]))

sum() doesn't know what a Square is. It doesn't need to — each shape knows its own area.

Try it yourself

Give three shapes an area() method and total them up.

Checkpoint

  • Polymorphism: one call, many implementations, chosen by the object.
  • It replaces chains of isinstance checks with methods on each class.
  • Duck typing means a shared parent class isn't required.
  • Built-ins like len() and + are polymorphic through dunder methods.

Questions about this lesson

Log in to ask a question.