Free lesson · Python Basic to Advance
What Polymorphism Means in Python
What Polymorphism Means in Python
What Polymorphism Is
Polymorphism - Greek for "many forms" - is the property where the same operation works on different types, producing type-appropriate results. The same + adds integers, concatenates strings, and merges lists. The same len() returns the size of strings, lists, dicts, and custom objects. The same for loop iterates over anything iterable.
def total_length(items):
return sum(len(item) for item in items)
print(total_length(["hello", "world"]))
# 10 (strings)
print(total_length([[1, 2], [3, 4, 5]]))
# 5 (lists)
print(total_length(("ab", "cd", "ef")))
# 6 (mixed iterable of strings)
This code calls len on different types within the same function. The function doesn't check what kind of objects it received - it just uses the operation. Each type implements len in its own way. The function works for any iterable of length-having objects. This is polymorphism in action.
The function is polymorphic because it doesn't depend on a specific type. It depends on operations - len() and iteration. Any object supporting those operations works.
Forms of Polymorphism
Three classic forms appear in programming languages:
- Ad-hoc polymorphism (overloading) - the same name behaves differently based on argument types. In Python, this is achieved via
@functools.singledispatchorisinstancechecks, since Python doesn't have C++/Java-style method overloading. - Parametric polymorphism (generics) - the same code works on any type uniformly. Python's
List[T]and generic functions fit this. - Subtype polymorphism (inheritance) - a subclass instance is usable where a parent instance is expected. The classic OOP form.
Python's most-used form is none of those exactly - it's duck typing, sometimes called structural polymorphism. An object qualifies for an operation if it supports the right methods, regardless of its formal type.
Duck Typing
The slogan: "if it walks like a duck and quacks like a duck, it's a duck." For code, this means: if it has the methods that the calling code needs, it can be used as if it were the expected type.
class Duck:
def quack(self):
return "quack!"
class Person:
def quack(self):
return "I'm quacking like a duck"
def make_it_quack(thing):
return thing.quack() # no type check - just call
print(make_it_quack(Duck()))
# quack!
print(make_it_quack(Person()))
# I'm quacking like a duck
This code uses duck typing. The function asks for a quack - anything with a quack method works. Duck and Person have no shared inheritance, no formal interface. They both qualify because they both have the required method. This is structural polymorphism - what matters is the shape, not the type.
This is Python's dominant style for polymorphism. Most well-written Python code doesn't ask "what type is this?" - it tries the operation and lets Python complain if the object doesn't support it.
Duck Typing vs Subtype Polymorphism
Subtype polymorphism requires a shared parent class:
class Animal:
def speak(self):
raise NotImplementedError
class Dog(Animal):
def speak(self):
return "woof"
class Cat(Animal):
def speak(self):
return "meow"
def let_them_speak(animals):
for a in animals:
print(a.speak())
let_them_speak([Dog(), Cat()])
# woof
# meow
This code uses subtype polymorphism. Dog and Cat both inherit from Animal. The function works on any Animal subclass. The shared base class is the polymorphism contract. Common in languages like Java; less central in Python.
Duck typing doesn't require the parent class:
class Dog:
def speak(self):
return "woof"
class Cat:
def speak(self):
return "meow"
class Robot: # no shared parent with Dog or Cat
def speak(self):
return "beep boop"
def let_them_speak(things):
for t in things:
print(t.speak())
let_them_speak([Dog(), Cat(), Robot()])
# woof
# meow
# beep boop
This code uses duck typing. Dog, Cat, and Robot share no inheritance. Each has a speak method. The function works on any of them. Duck typing is more flexible - new types qualify without modifying any existing hierarchy.
Python prefers duck typing because it's flexible. A new type can participate in a polymorphic function without inheriting from anything - just by having the right methods.
EAFP vs LBYL
Two related programming idioms:
- LBYL (Look Before You Leap) - check conditions before acting
- EAFP (Easier to Ask Forgiveness than Permission) - try the operation, handle the exception
Python favors EAFP, which fits duck typing naturally:
# LBYL - type-check first
def process_lbyl(obj):
if not isinstance(obj, dict):
raise TypeError("expected dict")
if "name" not in obj:
return "unknown"
return obj["name"]
# EAFP - try the operation, catch the error
def process_eafp(obj):
try:
return obj["name"]
except (KeyError, TypeError):
return "unknown"
This code contrasts the two idioms. LBYL checks conditions in advance - type, presence of key. EAFP just tries the operation and catches problems. EAFP fits duck typing because it accepts any object that supports the required operations. LBYL fits static type checking because it explicitly checks types.
EAFP supports more types - anything that behaves like a dict (custom classes, defaultdict, MappingProxyType, etc.). LBYL rejects them all unless explicitly checked.
EAFP isn't always right - for performance-critical inner loops where exceptions are expensive, LBYL can be faster. For readability and flexibility, EAFP usually wins.
Polymorphism Through Operators
Python's operators are polymorphic via dunder methods (Module 12.7-12.10 covered these):
print(1 + 2)
# 3 (int + int - arithmetic)
print("a" + "b")
# ab (str + str - concatenation)
print([1, 2] + [3, 4])
# [1, 2, 3, 4] (list + list - extension)
print({"a": 1} | {"b": 2}) # Python 3.9+
# {'a': 1, 'b': 2} (dict | dict - merge)
This code shows the plus operator working polymorphically. Each type's plus dunder implements the operation appropriately. Numbers add, strings concatenate, lists extend, dicts merge. The same operator works across all of them - each type defines its own semantics.
User-defined classes participate by implementing the right dunder methods. This is operator overloading - covered in detail in Topic 14.3.
Polymorphism Through Iteration
The iteration protocol is one of Python's most-used polymorphic interfaces:
# Anything iterable works
for x in [1, 2, 3]:
print(x)
for x in "abc":
print(x)
for x in range(3):
print(x)
for x in {"a", "b", "c"}:
print(x)
for k, v in {"a": 1, "b": 2}.items():
print(k, v)
This code uses the for loop polymorphically. Lists, strings, ranges, sets, and dict views all work. Each implements iter - the only requirement for participating. The iteration protocol is structural - any object with iter is iterable. Module 15 covers iterators in depth.
A custom class can join this polymorphic world by implementing __iter__:
class CountTo:
def __init__(self, n):
self.n = n
def __iter__(self):
for i in range(self.n):
yield i
for x in CountTo(3):
print(x)
# 0
# 1
# 2
This code makes a custom class iterable. The iter dunder uses a generator to yield values. The for loop treats the custom class identically to a list or range. New types join the polymorphic interface by implementing the protocol's dunder methods.
This is the Python pattern: protocols expressed as dunder methods. Any class that implements the dunders participates in the polymorphic operation.
When Type Hints Meet Polymorphism
Modern Python often combines duck typing with type hints. The Protocol class (Topic 14.4) lets type checkers verify duck-typed code:
from typing import Protocol
class Quacker(Protocol):
def quack(self) -> str: ...
def make_it_quack(thing: Quacker) -> str:
return thing.quack()
class Duck:
def quack(self) -> str:
return "quack"
class Person:
def quack(self) -> str:
return "I'm quacking"
# Both qualify - they have a quack method
print(make_it_quack(Duck()))
print(make_it_quack(Person()))
This code uses a Protocol to type-check duck typing. The Quacker protocol says "anything with a quack method returning a string." Type checkers verify that Duck and Person both satisfy it - without either inheriting from Quacker. Protocols bring static type checking to duck-typed code.
This is the modern Python idiom: write code that's duck-typed at runtime and Protocol-checked at static analysis time. Topic 14.4 covers Protocols in depth.
Polymorphism Across Built-ins
Python's polymorphism is pervasive in the built-ins:
len(x)- works on strings, lists, dicts, sets, custom classes with__len__iter(x)- works on anything with__iter__or__getitem__sorted(x)- works on any iterable of comparable elementssum(x)- works on any iterable of numbers (or anything with__add__)str(x),repr(x),bool(x),int(x), etc. - each dispatches to the corresponding dunder
The pattern: built-in functions are thin wrappers around dunder methods. Custom types participate by implementing the dunders.
Performance and Polymorphism
Polymorphism via dunder method dispatch adds method-lookup overhead per operation. For numeric work, this means Python's polymorphic operators are slower than C-level operations on native arrays.
NumPy and similar libraries solve this with vectorized operations: operate on a whole array at once at C speed, bypassing Python's polymorphic dispatch.
For ordinary code, the polymorphism overhead is irrelevant. For high-performance numerics, the answer is specialized libraries, not avoiding polymorphism.
Interview Relevance
Interview: Polymorphism is OOP fundamental:
- The duck-typing slogan and what it means in practice
- EAFP vs LBYL, and why Python favors EAFP
- The distinction between subtype polymorphism (inheritance) and structural polymorphism (duck typing)
- That built-in operators and functions are polymorphic via dunders
- The modern Protocol pattern that combines duck typing with type checking
- Iteration as a canonical Python polymorphic interface
A common interview moment: a candidate writes excessive
isinstancechecks. The right feedback is to rely on duck typing - let the object's methods do the work.Another moment: explaining duck typing to someone from a Java/C# background. The right framing involves "structural over nominal" - what matters is the methods, not the declared type.
Module 30 covers Python interview preparation.
Common Pitfalls
Warning: Excessive type checking. Defensive
isinstancechecks at the start of every function break duck typing and reject valid alternative types.Catching
Exceptionfor EAFP. Catch specific exceptions. A blanketexcept Exceptionhides bugs.Duck typing without documentation. Code that uses duck typing should document what methods or interface is expected. Without documentation, callers have to read the function body.
Mistaking duck typing for "no contract." Duck typing has a contract - the methods that must be present. The contract is just structural rather than nominal.
Using
hasattrinstead of try/except.hasattr(obj, "method")is a form of LBYL. EAFP - try calling and handle errors - is often more Pythonic, especially when the attribute might fail for reasons other than absence.Polymorphism via
isinstancechains. Long type-dispatch chains often signal a missing abstraction. Polymorphic method calls orfunctools.singledispatchare usually cleaner.
Try It
Exercise: Practice with polymorphism:
- Write a
describe(thing)function that callsthing.describe(). Define three unrelated classes that all have adescribemethod. Pass each to the function.- Convert an LBYL-style function (with
isinstanceandhasattrchecks) to EAFP (with try/except).- Make a custom class iterable by implementing
__iter__. Use it in aforloop and withlist().- Use the
len()built-in on a string, a list, a dict, and a custom class with__len__. Verify each returns a sensible value.- Implement a
QuackerProtocol from typing and a function accepting it. Define two classes withquack()methods (not inheriting fromQuacker) and verify both type-check.- Write a function
total = sum(items, start=...). Try it with a list ofMoneyobjects you define (with__add__). Watch what happens; understand why thestartparameter matters.