AlgoInsight

Free lesson · Python Basic to Advance

What Polymorphism Means in Python

Medium8 min readpython

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:

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:

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:

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 isinstance checks. 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 isinstance checks at the start of every function break duck typing and reject valid alternative types.

Catching Exception for EAFP. Catch specific exceptions. A blanket except Exception hides 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 hasattr instead 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 isinstance chains. Long type-dispatch chains often signal a missing abstraction. Polymorphic method calls or functools.singledispatch are usually cleaner.

Try It

Exercise: Practice with polymorphism:

  1. Write a describe(thing) function that calls thing.describe(). Define three unrelated classes that all have a describe method. Pass each to the function.
  2. Convert an LBYL-style function (with isinstance and hasattr checks) to EAFP (with try/except).
  3. Make a custom class iterable by implementing __iter__. Use it in a for loop and with list().
  4. Use the len() built-in on a string, a list, a dict, and a custom class with __len__. Verify each returns a sensible value.
  5. Implement a Quacker Protocol from typing and a function accepting it. Define two classes with quack() methods (not inheriting from Quacker) and verify both type-check.
  6. Write a function total = sum(items, start=...). Try it with a list of Money objects you define (with __add__). Watch what happens; understand why the start parameter matters.

Back to Python Basic to Advance