Skip to main content
Python · lesson 14 of 34
Python beginner Lesson 14 of 34

Polymorphism and Duck Typing in Python

See how Python's polymorphism works through duck typing, letting unrelated objects share behavior without a common base class or interface.

By SkillByExample · Updated

Many languages implement polymorphism by requiring classes to share an interface or base class. Python takes a looser approach: if an object responds to the method you’re calling, it works — regardless of its type. Output below is from Python 3.13.

Same Interface, Different Behavior

Three unrelated classes each define a send method. Code that calls send doesn’t need to know or care which one it has.

class EmailNotifier:
    def send(self, message):
        return f"Emailing: {message}"

class SmsNotifier:
    def send(self, message):
        return f"Texting: {message}"

class PushNotifier:
    def send(self, message):
        return f"Push alert: {message}"

notifiers = [EmailNotifier(), SmsNotifier(), PushNotifier()]

for n in notifiers:
    # Same call, same method name, three different behaviors - the caller
    # never checks which class it's holding.
    print(n.send("Server restarted"))
$ python notifiers.py
Emailing: Server restarted
Texting: Server restarted
Push alert: Server restarted

None of these classes inherit from a common Notifier base or implement a declared interface. They’re polymorphic purely because they each happen to define a method called send with a compatible signature.

Duck Typing: No Shared Ancestor Required

Python doesn’t check an object’s type before calling a method on it — it just tries the call. This example goes further: the classes below aren’t even conceptually related.

class Duck:
    def make_sound(self):
        return "Quack!"

class ToyDuck:                        # unrelated class - no shared base, no inheritance
    def make_sound(self):
        return "Squeak! (it's a toy)"

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

def announce(thing):
    # No isinstance() check anywhere - if it has make_sound(), this works.
    print(thing.make_sound())

for creature in [Duck(), ToyDuck(), Dog()]:
    announce(creature)

class Robot:
    pass  # no make_sound() at all

try:
    announce(Robot())
except AttributeError as e:
    print(f"AttributeError: {e}")
$ python duck_typing.py
Quack!
Squeak! (it's a toy)
Woof!
AttributeError: 'Robot' object has no attribute 'make_sound'

announce() never asks what type thing is — it just calls .make_sound() and trusts the object to respond. That’s duck typing’s entire contract: match by behavior, not by declared type. The tradeoff shows up with Robot: there’s no interface Python could have checked in advance, so the mismatch only surfaces once announce() actually tries the call.

Practice

1. Add a WebhookNotifier class that also defines send, without changing the loop. Confirm it slots in with zero other changes.

Solution
class EmailNotifier:
    def send(self, message):
        return f"Emailing: {message}"

class SmsNotifier:
    def send(self, message):
        return f"Texting: {message}"

class WebhookNotifier:
    def send(self, message):
        return f"POSTing webhook: {message}"

notifiers = [EmailNotifier(), SmsNotifier(), WebhookNotifier()]
for n in notifiers:
    print(n.send("Server restarted"))
$ python webhook.py
Emailing: Server restarted
Texting: Server restarted
POSTing webhook: Server restarted

The loop only depends on the method name send, so a new notifier needs no registration, base class or change to the calling code.

2. Write a function total_area(shapes) that sums .area() across a Square and a Circle, using duck typing and no shared base class.

Solution
import math

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

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

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2

def total_area(shapes):
    return sum(s.area() for s in shapes)

print(round(total_area([Square(4), Circle(2)]), 2))
$ python total_area.py
28.57

16 from the square plus about 12.57 from the circle. total_area would accept any object with an area() method, including classes written long after it.

3. Rewrite announce() to handle a missing make_sound with getattr(thing, "make_sound", None) instead of try/except, and compare the two approaches.

Solution
class Duck:
    def make_sound(self):
        return "Quack!"

class ToyDuck:
    def make_sound(self):
        return "Squeak! (it's a toy)"

class Robot:
    pass

def announce(thing):
    sound_fn = getattr(thing, "make_sound", None)
    if sound_fn is None:
        print(f"{type(thing).__name__} can't make a sound")
    else:
        print(sound_fn())

announce(Duck())
announce(Robot())
$ python getattr_announce.py
Quack!
Robot can't make a sound

getattr with a default checks for the method up front and branches (“look before you leap”). try/except assumes the method exists and handles the failure afterwards (“easier to ask forgiveness than permission”), which is the more common Python style.

4. Write a strict_announce() that only accepts Duck instances via isinstance(), and explain when you would prefer this over duck typing.

Solution
class Duck:
    def make_sound(self):
        return "Quack!"

class ToyDuck:
    def make_sound(self):
        return "Squeak! (it's a toy)"

class Robot:
    pass

def strict_announce(thing):
    if not isinstance(thing, Duck):
        raise TypeError(f"{type(thing).__name__} is not supported")
    print(thing.make_sound())

strict_announce(Duck())
try:
    strict_announce(ToyDuck())
except TypeError as e:
    print(f"TypeError: {e}")
$ python strict_announce.py
Quack!
TypeError: ToyDuck is not supported

ToyDuck has a perfectly good make_sound(), but the type check rejects it anyway. Prefer isinstance() when accepting any object with the right method would be a bug (a toy slipping in where only real animals belong); duck typing trades that safety for flexibility.

Frequently Asked Questions

What is polymorphism in Python?
Polymorphism means objects of different types can be used through the same interface — calling the same method name on each produces behavior appropriate to that object, without the caller checking which type it is.
What is duck typing?
Duck typing is Python's approach to polymorphism: if an object has the method or attribute you need, you can use it, regardless of its class or whether it shares a base class with anything else. "If it walks like a duck and quacks like a duck, it's a duck."
Do I need inheritance for polymorphism in Python?
No. Unlike languages that require implementing a shared interface, Python only checks at call time whether the object has the method being called. Two completely unrelated classes can be used polymorphically as long as both define that method.
What happens if an object doesn't support the method being called?
Python raises an AttributeError at the point the method is called, not earlier. There's no compile-time check — the tradeoff for duck typing's flexibility is that mismatches surface at runtime.