Skip to content

Polymorphism & Duck Typing

Polymorphism (from Greek, meaning “many forms”) is the ability of different objects to respond to the same method call in their own way. It allows you to write code that can work with many types of data without needing to know the exact class of each object.

In Python, polymorphism is driven by two main concepts: Method Overriding and Duck Typing.


This occurs when a subclass provides its own implementation of a method that is already defined in its parent class.

shapes.py
class Shape:
def draw(self):
raise NotImplementedError("Subclasses must implement draw()")
class Circle(Shape):
def draw(self):
print("Drawing a circle ⚪")
class Square(Shape):
def draw(self):
print("Drawing a square ⬛")
# A polymorphic function
def render(shape_obj):
shape_obj.draw()
render(Circle()) # Drawing a circle ⚪
render(Square()) # Drawing a square ⬛

Unlike Java or C++, Python doesn’t strictly require objects to share a parent class to be polymorphic. Python follows the Duck Typing philosophy:

“If it walks like a duck and quacks like a duck, it’s a duck.”

If an object has the required method, Python will call it, regardless of the object’s inheritance.

duck_typing.py
class PDFDocument:
def print(self):
print("Printing PDF...")
class Spreadsheet:
def print(self):
print("Printing Spreadsheet...")
# These don't share a parent, but they both have a .print() method
def output_data(doc):
doc.print()
output_data(PDFDocument())
output_data(Spreadsheet())

3. Why Python doesn’t have “Method Overloading”

Section titled “3. Why Python doesn’t have “Method Overloading””

In languages like C++, you can have multiple versions of the same function with different parameters: add(int x, int y) and add(float x, float y, float z).

Python does not support this. If you define two functions with the same name, the second one simply overwrites the first.

Instead, Python achieves the same goal using default arguments or *args. This is a form of polymorphism where a single function signature can handle many “forms” of input.


When you call obj.method(), Python doesn’t know at “compile time” which method will run. Instead, it performs a Dynamic Dispatch at runtime:

  1. It looks at the actual object in memory.
  2. It looks up the method in that object’s class.
  3. If not found, it traverses the inheritance tree (MRO).

This late binding is what makes Python so flexible but also slightly slower than languages that “hard-wire” the function calls during compilation.


FeatureDescription
OverridingChild replaces Parent’s method logic.
Duck TypingBehavior-based matching, ignoring class hierarchy.
Dynamic DispatchDeciding which method to run at runtime.
Unified InterfaceUsing the same name (e.g., .draw()) for different actions.