Python course menu
Python Polymorphism: One Interface, Many Forms (with Duck Typing)
Learn polymorphism in Python: call the same method on different objects, method overriding in subclasses, duck typing, built-in polymorphism like len() and +, and operator overloading with special methods.
On this page
Polymorphism ("many forms") means different kinds of objects can be used through the same interface. You call shape.area() without caring whether the shape is a circle or a square; each object knows how to calculate its own area. This lets you write general code that works with many types, including types written later.
The same method, different behaviour#
class Dog:
def speak(self):
return "Woof"
class Cat:
def speak(self):
return "Meow"
class Duck:
def speak(self):
return "Quack"
for animal in [Dog(), Cat(), Duck()]:
print(type(animal).__name__, animal.speak())Dog Woof
Cat Meow
Duck QuackThe loop does not check types or use if statements. It simply calls speak(), and each object responds in its own way.
Polymorphism through inheritance#
Usually, related classes share a parent that defines the common interface:
import math
class Shape:
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return round(math.pi * self.r ** 2, 2)
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
class Triangle(Shape):
def __init__(self, b, h):
self.b, self.h = b, h
def area(self):
return 0.5 * self.b * self.h
shapes = [Circle(1), Square(3), Triangle(4, 5)]
for s in shapes:
print(type(s).__name__, s.area())
print(sum(s.area() for s in shapes))Circle 3.14
Square 9
Triangle 10.0
22.14Adding a new Hexagon class with an area() method requires no changes to the loop or the total. That is the real benefit: code that is open to new types without being rewritten.
Duck typing#
Python does not require a shared parent class. If an object has the method you call, it works: "if it walks like a duck and quacks like a duck, it's a duck."
class File:
def write(self, text):
return f"file <- {text}"
class Screen:
def write(self, text):
return f"screen <- {text}"
def save(target, text):
return target.write(text)
print(save(File(), "hi"), "|", save(Screen(), "hi"))file <- hi | screen <- hisave() works with anything that has a write() method. Python's own functions rely on this constantly: anything you can loop over works in a for loop, whatever its class.
Polymorphism you already use#
Built-in functions and operators are polymorphic:
print(len("hello"), len([1, 2, 3]), len({"a": 1}))
print(1 + 2, "a" + "b", [1] + [2])5 3 1
3 ab [1, 2]len() and + mean the right thing for each type.
Making your own classes work with operators#
Special methods let your objects support built-in operations. __add__ defines +:
class Money:
def __init__(self, amount):
self.amount = amount
def __add__(self, other):
return Money(self.amount + other.amount)
def __str__(self):
return f"₹{self.amount:,}"
total = Money(1500) + Money(2750)
print(total)₹4,250__len__ and __getitem__ make an object behave like a sequence:
class Playlist:
def __init__(self, songs):
self.songs = songs
def __len__(self):
return len(self.songs)
def __getitem__(self, i):
return self.songs[i]
p = Playlist(["Song A", "Song B", "Song C"])
print(len(p), p[1])
for s in p:
print(s)3 Song B
Song A
Song B
Song CDefining __getitem__ was enough for for loops to work too.
Common mistakes#
Practice#
Practice exercise
Create a Notification base class with a send(msg) method, and two subclasses: Email (returns Email to <address>: <msg>) and SMS (returns SMS to <number>: plus only the first 20 characters of the message). Send the same message through both in one loop.
Show hint
Store the recipient in the base class's __init__. Each subclass overrides send. Use slicing msg[:20] for the SMS.
Show solution
class Notification:
def __init__(self, to):
self.to = to
def send(self, msg):
raise NotImplementedError
class Email(Notification):
def send(self, msg):
return f"Email to {self.to}: {msg}"
class SMS(Notification):
def send(self, msg):
return f"SMS to {self.to}: {msg[:20]}"
for n in [Email("asha@x.com"), SMS("+91 98450 12345")]:
print(n.send("Your order #101 has been shipped today"))Email to asha@x.com: Your order #101 has been shipped today
SMS to +91 98450 12345: Your order #101 hasThe loop treats both the same way; each class decides how to deliver the message.
Key takeaway#
Finished this lesson?