Back to Blog
Python

Python Polymorphism: How It Works and When to Use It

python polymorphism: Learn how Python polymorphism works through duck typing, ABCs, and protocols, and see practical examples for designing flexible, maintainable code.

polymorphismduck typingabstract base classesprotocolsobject-oriented pythontype hints
Illustration of Python polymorphism showing multiple object types responding to the same method call.

Python polymorphism is often described as "one interface, many implementations." In practice, it means that different objects can respond to the same method call in their own way, and the Python runtime decides which implementation to invoke based on the object's type. This behavior is central to designing extensible code, but it works differently than in statically typed languages like Java or C#. Understanding those differences helps you write code that is both flexible and maintainable.

How Python Implements Polymorphism Without Static Types

Python does not require classes to share a base type for polymorphism to work. The runtime checks whether an object has the expected method or attribute when the call is made. This is called duck typing: if an object implements the method you call, it is accepted.

class Dog: def speak(self): return "Woof" class Cat: def speak(self): return "Meow" def make_speak(animal): print(animal.speak()) make_speak(Dog()) make_speak(Cat())

Both Dog and Cat are unrelated classes, yet make_speak works with either. The function only requires that the argument has a speak method. This is the simplest form of polymorphism in Python, and it is used throughout the standard library.

Method Overriding in Inheritance

When you use inheritance, a subclass can replace a method defined in its parent. This is method overriding, and it is the classic object-oriented form of polymorphism.

class Shape: def area(self): raise NotImplementedError class Rectangle(Shape): def __init__(self, width, height): self.width = width self.height = height def area(self): return self.width * self.height class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14159 * self.radius ** 2

Calling area() on a Rectangle or Circle dispatches to the correct implementation. The base class method is never called directly in this example, but it documents the expected interface. In Python, you do not need to declare a method as virtual; every method can be overridden.

Abstract Base Classes for Enforced Contracts

If you want to guarantee that every subclass implements a method, use an abstract base class (ABC). The abc module provides the machinery to enforce this at instantiation time.

from abc import ABC, abstractmethod class Payment(ABC): @abstractmethod def charge(self, amount): pass class CreditCard(Payment): def charge(self, amount): return f"Charged ${amount} to credit card" class PayPal(Payment): def charge(self, amount): return f"Charged ${amount} via PayPal"

Any class that inherits from Payment but does not implement charge cannot be instantiated. This catches missing methods early and makes the contract explicit. ABCs are useful when you control the class hierarchy and want to enforce a common interface.

Protocols for Structural Typing

Python 3.8 introduced typing.Protocol for structural subtyping. A protocol defines a set of methods or attributes, and any class that implements them is considered a subtype, even without inheritance.

from typing import Protocol class Speaker(Protocol): def speak(self) -> str: ... class Dog: def speak(self) -> str: return "Woof" def make_speak(animal: Speaker) -> None: print(animal.speak())

Here Dog does not inherit from Speaker, but it satisfies the protocol. Static type checkers like mypy recognize this relationship. At runtime, protocols are not enforced unless you use @runtime_checkable and isinstance(). This gives you the flexibility of duck typing with the safety of static analysis.

Polymorphism with Built-in Functions and Operators

Python's built-in functions and operators rely on special methods to achieve polymorphism. For example, len() calls __len__, and the + operator calls __add__.

class Vector: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): return Vector(self.x + other.x, self.y + other.y) def __len__(self): return 2

This allows Vector instances to work with + and len() exactly like built-in types. When you design custom classes, implementing these special methods makes them integrate naturally with Python's standard behavior.

Runtime Cost and Dispatch Mechanism

Polymorphism in Python is resolved at runtime. When you call a method, Python looks up the attribute on the object's type and then invokes it. This lookup is cached for performance, so repeated calls to the same method on the same type are fast. The overhead is small and rarely a bottleneck in application code. The real cost is the absence of compile-time checking: a missing method only fails when the call is executed. This is a tradeoff: you gain flexibility, but you lose some early error detection.

Choosing Between Duck Typing, ABCs, and Protocols

The right approach depends on the contract you need.

ApproachWhen to useEnforcement
Duck typingSmall scripts, internal code, or when the interface is trivialNone at runtime
ABCYou control the class hierarchy and want to force subclasses to implement methodsAt instantiation
ProtocolYou want static type checking without forcing inheritanceStatic only (unless runtime_checkable)

Use duck typing when the interface is obvious and the code is not part of a public API. Use ABCs when you need a shared base class and want to prevent incomplete implementations. Use protocols when you want to define a structural contract that external classes can satisfy without inheriting from your code.

Common Pitfalls and How to Avoid Them

A frequent mistake is to use isinstance() to check for a specific concrete type, which breaks polymorphism. Instead, rely on the object's behavior or use an ABC or protocol for the check. Another issue is violating the Liskov substitution principle: a subclass method should accept the same arguments and return a compatible result as the parent method. If a subclass narrows the accepted input types, code that works with the parent may fail with the subclass. Keep overridden methods consistent with their base class signatures.

When you design for polymorphism, think about the contract you actually need. If you only need a method to exist, duck typing works. If you need to enforce a contract across many classes, ABCs or protocols give you structure without sacrificing Python's dynamic nature.

python polymorphism: Practical Usage and Code Examples | RYUSLOG DEV