Python · lesson 14 of 34
Beginner
16- 1 Introduction to Python
- 2 Setting Up Python
- 3 Python Variables and Type System
- 4 Python Data Types
- 5 Python Operators
- 6 Python Strings
- 7 Control Flow in Python
- 8 Python Functions and Parameters
- 9 Python Data Structures
- 10 Python Collections
- 11 Python Modules and Packages
- 12 Object-Oriented Programming in Python
- 13 Encapsulation and Name Mangling in Python
- 14 Polymorphism and Duck Typing in Python
- 15 Abstraction with Abstract Base Classes in Python
- 21 Building a Python CLI Tool
Intermediate
12- 16 Multiple Inheritance and MRO in Python
- 17 Composition vs Inheritance in Python
- 18 Python Dataclasses
- 19 Error Handling in Python
- 20 File I/O, pathlib, and JSON in Python
- 22 Python Decorators and Closures
- 23 Python Generators
- 24 Python Context Managers
- 25 Python Concurrency
- 27 Testing Python Code
- 28 Python Security
- 34 Python Interview Prep
Advanced
6Polymorphism 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.