Python Polymorphism
Tutorial 23 of 65 · pythondeck.com Python course
Polymorphism in Python is mostly duck typing: if it walks like a duck and quacks like a duck, it is a duck. Functions work on any object implementing the required protocol. Overriding methods in subclasses gives classic OOP polymorphism.
Polymorphism lets one interface work with many concrete types. In Python that usually means duck typing: you call methods or operators on whatever object is passed in, as long as it supports the expected protocol. That keeps libraries flexible and avoids rigid class hierarchies.
Subclass polymorphism still matters when you override methods in a child class and callers use the parent type. Combined with abstract base classes, you can document required methods while staying dynamically typed at runtime.
Duck typing: behaviour is defined by available methods, not by inheritance alone.
Method overriding in subclasses for classic OOP polymorphism.
Operator overloading via dunder methods such as __add__ and __repr__.
Abstract base classes (abc.ABC, @abstractmethod) to formalise interfaces.
Protocols and structural typing with typing.Protocol for static checkers.
When to prefer composition over deep inheritance trees.
Design APIs around small, stable protocols (e.g. "has read()") instead of checking concrete types with isinstance everywhere. That makes testing easier because you can pass simple fakes or mocks.
Multiple inheritance and the method resolution order (MRO) affect which super() implementation runs. Trace Cls.__mro__ when override chains get confusing.
Static type checkers complement duck typing: annotate parameters with Protocol or union types so IDEs catch missing methods before runtime.
Using isinstance on many concrete classes instead of relying on a shared protocol.
Treating "inherits from X" as proof that required methods exist (parent may not define them).
Overusing multiple inheritance without understanding MRO and super() call order.
Catching all exceptions in polymorphic dispatch code and hiding the real failure.
Define minimal interfaces (one or two methods) and document them in docstrings or ABCs.
Write tests with stand-in objects that implement only the protocol you need.
Prefer composition and delegation when behaviour varies more than type identity.
Use typing.Protocol when mypy or Pyright should verify structural compatibility.
Re-read the examples below with these ideas in mind; change variable names and inputs to match your own project.
The program below demonstrates duck typing. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# Example: Duck typing
# Run in the REPL or save as a .py file and execute with python.
class File:
def read(self):
return "file data"
class Net:
def read(self):
return "network data"
for src in [File(), Net()]:
print(src.read())
This sample walks through operator overload in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# Example: Operator overload
# Run in the REPL or save as a .py file and execute with python.
class Vec:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other):
return Vec(self.x + other.x, self.y + other.y)
def __repr__(self):
return f"Vec({self.x},{self.y})"
print(Vec(1,2) + Vec(3,4))
Here is a hands-on illustration of abc. Follow the inline comments first; only then execute the snippet and compare the result with what you expected.
# Example: ABC
# Run in the REPL or save as a .py file and execute with python.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): ...
class Square(Shape):
def __init__(self, s): self.s = s
def area(self): return self.s ** 2
print(Square(4).area())
The program below demonstrates duck typing. Read the comments on each line, run the code, then change names or values to see how the output shifts.
# Polymorphism: same interface, different concrete types
class Cat: # feline
def speak(self): return "meow" # method
class Robot: # machine
def speak(self): return "beep" # same method name
def announce(entity): # accepts any object with speak()
print(entity.speak()) # duck typing — no shared base required
for thing in (Cat(), Robot()): # heterogeneous sequence
announce(thing) # meow / beep
This sample walks through protocol check in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.
# isinstance(obj, Protocol) checks structural typing (3.8+)
from typing import Protocol # typing support
class SupportsClose(Protocol): # structural interface
def close(self) -> None: ... # required method
class FileLike: # conforms structurally
def close(self): print("closed") # implement method
def cleanup(resource): # generic cleanup helper
if isinstance(resource, SupportsClose): # protocol check
resource.close() # call without inheritance
cleanup(FileLike()) # prints closed