Pillar Four: Polymorphism in Python
Polymorphism means the same method name or operator can behave differently depending on the object it's used with. Python achieves this through duck typing (any object with the right method works, regardless of type) and dunder methods like __add__ and __eq__, which let your own classes plug into built-in operators and functions.
Back in The Four Pillars of OOP's coffee machine story, polymorphism was the idea that pressing "Brew" on a drip machine and pressing "Brew" on an espresso machine triggers two completely different sequences of actions, even though the button says the exact same word. The name stays the same; the behavior changes depending on which machine you're using.
That's the whole idea behind this final pillar: the same name, used on different types of objects, can do different things — and that's a deliberate, powerful feature, not a bug. This article covers several flavors: polymorphism through inheritance, "duck typing," and operator overloading you build yourself.
What you will learn
- Polymorphism you already get for free through inheritance
- "Duck typing" — Python's willingness to call any method that exists, regardless of type
- How built-ins like
len()use polymorphism via__len__ - Operator overloading — teaching
+,==, and more to understand your own classes - A final example combining all four pillars in one small program
Polymorphism you already have, through inheritance
Remember move_cards from Inheritance? It called hand.add_card() without checking whether hand was actually a Hand or a plain Deck — it just trusted the method would be there. That trust is polymorphism: the same code working across related types without caring which one it's holding, safe because Hand and Deck share a common ancestor.
Duck typing: "if it walks like a duck..."
Python doesn't actually care what type an object officially is — only whether it can actually do the thing you're asking, right now. This idea is nicknamed duck typing: "if it walks like a duck and quacks like a duck, it's a duck." In Python terms: "if it has the method I'm trying to call, I don't care what class it officially belongs to."
class Duck:
def make_sound(self):
return "Quack!"
class Dog:
def make_sound(self):
return "Woof!"
class Car:
def make_sound(self):
return "Vroom!"
def make_it_speak(thing):
print(thing.make_sound())
make_it_speak(Duck()) # Quack!
make_it_speak(Dog()) # Woof!
make_it_speak(Car()) # Vroom!Duck, Dog, and Car are completely unrelated classes — none inherit from each other, none share a common parent. Yet make_it_speak works perfectly with all three, because it never cares what type thing is — only that it has a make_sound() method. This is polymorphism completely unconnected to inheritance — pure duck typing, a meaningfully different approach from languages that insist on shared ancestors or declared interfaces.
The built-in functions that already use polymorphism
len("hello") # 5 (works on strings)
len([1, 2, 3]) # 3 (works on lists)
len({'a': 1, 'b': 2}) # 2 (works on dictionaries)
len({1, 2, 3}) # 3 (works on sets)len() is a single function that correctly handles completely different types. How? Under the hood, every one of these types implements a special method called __len__, and len() simply calls .__len__() on whatever you hand it. Each type defines __len__ its own way, but len() itself doesn't need to know the details — it just trusts the method exists. sorted(), print(), str(), and in all work this same general way.
Operator overloading: teaching + to understand your own classes
So far, + only knows how to work with things Python already understands. What if you wanted + to work on a Time, combining it with a duration in seconds?
def int_to_time(seconds):
minutes, second = divmod(seconds, 60)
hour, minute = divmod(minutes, 60)
return Time(hour, minute, second)
# inside class Time:
def __add__(self, other):
seconds = self.time_to_int() + other
return int_to_time(seconds)The moment Time has an __add__ method, + quietly starts working on it:
start = Time(9, 45, 0)
duration = 1500 # seconds
print(start + duration)
# → 10:10:00Python sees start + duration, notices start is a Time, and silently translates that into start.__add__(duration). This translation is called operator overloading — a specific, powerful flavor of polymorphism: the exact same + symbol means something different depending on the types on either side. As covered in Classes and Methods, you can combine this with isinstance to handle both Time + Time and Time + int, plus __radd__ to catch the reversed case 1500 + start. Once __add__ is implemented, sum() works on a list of Time objects for free.
Other useful special methods
__add__ is just one of a family of special methods (sometimes nicknamed "dunder methods," short for "double underscore"), each hooking into a different built-in behavior:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
"""Called by print() and str()"""
return f'({self.x}, {self.y})'
def __eq__(self, other):
"""Called by =="""
return self.x == other.x and self.y == other.y
def __add__(self, other):
"""Called by +"""
return Point(self.x + other.x, self.y + other.y)
def __len__(self):
"""Called by len() — here, distance from origin, rounded down"""
return int((self.x ** 2 + self.y ** 2) ** 0.5)p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)
print(p1) # (1, 2) uses __str__
print(p1 == p2) # True uses __eq__
print(p1 == p3) # False uses __eq__
print(p1 + p3) # (4, 6) uses __add__
print(len(p3)) # 5 uses __len__This connects directly to something from Classes and Objects: without __eq__, Python's default == checks identity, not content — two separate Point objects with identical coordinates would be "not equal" simply because they're different objects in memory. Defining __eq__ teaches Python what "equal" actually means for your class.
Polymorphism working together with inheritance
Polymorphism becomes especially elegant combined with inheritance and abstract classes, like Shape, Circle, and Rectangle from Abstraction:
shapes = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
print(f"Area: {shape.area()}, Perimeter: {shape.perimeter()}")
# Area: 78.53975, Perimeter: 31.4159
# Area: 24, Perimeter: 20This loop calls .area() and .perimeter() on every shape — but Circle.area() and Rectangle.area() compute completely different formulas underneath. The loop never needed an if isinstance(...): ... elif isinstance(...): ... chain. It simply trusts that whatever shape is, calling .area() does the right thing — polymorphism delivering its full promise: one piece of code, correctly handling many types, with zero special-casing.
Putting it all together: all four pillars, in one place
from abc import ABC, abstractmethod
# ABSTRACTION — a contract that all animals must follow
class Animal(ABC):
def __init__(self, name):
self._name = name # ENCAPSULATION — protected, accessed via property
@property
def name(self):
return self._name
@abstractmethod
def make_sound(self):
"""Every animal must define its own sound."""
pass
def __str__(self):
return f"{self.name} says {self.make_sound()}"
# INHERITANCE — Dog and Cat build on top of Animal
class Dog(Animal):
def make_sound(self):
return "Woof!"
class Cat(Animal):
def make_sound(self):
return "Meow!"
# POLYMORPHISM — the same loop, same method calls, different behavior each time
animals = [Dog("Rex"), Cat("Whiskers")]
for animal in animals:
print(animal)
# Rex says Woof!
# Whiskers says Meow!- Encapsulation —
_nameis protected, only reachable through thenameproperty. - Abstraction —
Animaldefines a contract (make_soundmust exist) without implementing it, andAnimal()can never be created directly. - Inheritance —
DogandCatboth build onAnimal, inheriting__str__and thenameproperty unchanged. - Polymorphism — the same loop, calling the same
print(animal), produces different output for each object, becausemake_sound()means something different depending on which animal it's called on.
Four separate ideas, each solving its own problem, all working together inside one small, clean piece of code.
Common mistakes
- Writing an
if isinstance(...): ... elif isinstance(...): ...chain instead of trusting a shared interface — this is exactly what polymorphism is meant to eliminate - Forgetting
__radd__, soint + objfails even thoughobj + intworks - Defining
__eq__but expectinghash()or set membership to keep working automatically — they often need__hash__too - Assuming duck typing requires any inheritance relationship at all — it explicitly does not
Common questions
What is polymorphism in Python?
Polymorphism means the same method name, function, or operator can behave differently depending on the type of object it's used with. In Python this is enabled both through inheritance and through duck typing.
What is duck typing in Python?
Duck typing means Python doesn't check an object's type before calling a method — it just tries to call the method, and if the object has it, the call succeeds. This works even for completely unrelated classes with no shared parent.
How does len() work on different types like strings and lists?
len() calls the __len__ method on whatever object you give it. Each type — string, list, dict, set — implements __len__ its own way, but len() itself doesn't need to know those details; it just trusts the method exists.
How do you make the + operator work on your own class in Python?
Define an __add__ method on the class. When Python evaluates a + b, it calls a.__add__(b). You can also define __radd__ to handle the reversed case, like when a plain number appears on the left side of the +.
Why does == return False for two objects with the same values by default?
Without a custom __eq__ method, Python's default == checks whether two objects are literally the same object in memory (identity), not whether their attribute values match. Defining __eq__ lets you specify what equality actually means for your class.
Explore
Related topics
Keep going — these sit next to this concept in a real learning path.
The Four Pillars of OOP
The overview these four deep-dive articles build from.
Pillar Three: Abstraction
The shared contract that makes polymorphism reliable.
Classes and Methods in Python
self, __init__, and the basics of operator overloading.
Python course hub
All free Python explainers and the path into Agentic practice.