Skip to content
Laviyx Learn

Try

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.

Intermediate9 min readUpdated
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#

Python
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())
Output
Dog Woof
Cat Meow
Duck Quack

The 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:

Python
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))
Output
Circle 3.14
Square 9
Triangle 10.0
22.14

Adding 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."

Python
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"))
Output
file <- hi | screen <- hi

save() 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:

Python
print(len("hello"), len([1, 2, 3]), len({"a": 1}))
print(1 + 2, "a" + "b", [1] + [2])
Output
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 +:

Python
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)
Output
₹4,250

__len__ and __getitem__ make an object behave like a sequence:

Python
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)
Output
3 Song B
Song A
Song B
Song C

Defining __getitem__ was enough for for loops to work too.

Special methodEnables
__str__, __repr__print(obj), repr(obj)
__eq__, __lt__==, < (and sorting)
__add__, __sub__, __mul__+, -, *
__len__len(obj)
__getitem__obj[i] and iteration
__contains__x in obj

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
Python
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"))
Output
Email to asha@x.com: Your order #101 has been shipped today
SMS to +91 98450 12345: Your order #101 has

The loop treats both the same way; each class decides how to deliver the message.

Key takeaway#

Finished this lesson?