Polymorphism means "many forms": the same interface behaves differently depending on the object behind it. The + operator adds numbers but joins strings. Polymorphism is what lets you add new types to a system without rewriting the code that uses them.
Duck typing#
Python doesn't check an object's type before calling a method. It only checks whether the method exists. "If it walks like a duck and quacks like a duck, it's a duck."
class Dog:
def speak(self):
print("Woof!")
class Cat:
def speak(self):
print("Meow!")
class Duck:
def speak(self):
print("Quack!")
# No common parent at all
for animal in [Dog(), Cat(), Duck()]:
animal.speak()
# Woof!
# Meow!
# Quack!
Python looks up speak on each object (instance, then its class and parents). If it finds it, it calls it; if not, you get AttributeError.
Method overriding#
Subclasses redefine a parent method with the same name:
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 Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
for shape in [Circle(5), Square(4)]:
print(round(shape.area(), 2))
# 78.54
# 16
Overriding replaces the parent's behavior entirely. Extending calls super().method() first and then adds to it.
Operator overloading#
Operators are syntactic sugar for dunder methods:
2 + 3 # (2).__add__(3)
"hi" + "!" # "hi".__add__("!")
[1] + [2] # [1].__add__([2])
You can give your own classes operators:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
# Return a Vector (not a tuple) so v1 + v2 + v3 keeps working
return Vector(self.x + other.x, self.y + other.y)
v = Vector(1, 2) + Vector(3, 4) + Vector(1, 1)
print(v.x, v.y) # 5 7
| Operator | Dunder method | Example |
|---|---|---|
+ |
__add__ |
v1 + v2 |
- |
__sub__ |
v1 - v2 |
* |
__mul__ |
v1 * 2 |
== |
__eq__ |
v1 == v2 |
< |
__lt__ |
v1 < v2 |
> |
__gt__ |
v1 > v2 |
str(obj) |
__str__ |
print(v1) |
len(obj) |
__len__ |
len(v1) |
See Dunder Methods & @property for more.
Duck typing vs overriding#
Python gives you polymorphism in two ways:
| Duck typing | Method overriding | |
|---|---|---|
| Needs a common parent? | No | Yes |
| Same method name? | Yes | Yes |
| Different behavior? | Yes | Yes |
| How the method is found | Normal lookup on the object | Child class first, then parent |
Why it matters in design#
Polymorphism replaces type checks with a single call:
# Without polymorphism: every new type means editing this block
for animal in animals:
if type(animal) == Dog:
animal.bark()
elif type(animal) == Cat:
animal.meow()
# With polymorphism: new types just implement speak()
for animal in animals:
animal.speak()
You'll see this in nearly every LLD problem:
- Parking lot: different vehicle types behind one
Vehicleinterface. - Ride booking: different cab types, one
calculate_fare(). - Payments: different payment methods, one
pay().
Key takeaways#
- Polymorphism = same interface, different behavior.
- Duck typing: Python checks that the method exists, not the type.
- Overriding: a child redefines a parent's method;
super()lets you extend it instead. - Every operator maps to a dunder method you can implement.
- Return your own type from operator methods so expressions chain.
- Polymorphism lets you add types without changing existing callers (the Open/Closed Principle).