1. Introduction
Polymorphism (from Greek, "many forms") means that the same operation or method call can behave differently depending on the object it is applied to. In Python, this shows up when different classes implement a method with the same name, and calling that method on each object produces behavior specific to that object's class.
Cricket analogy: Polymorphism is like the same call of 'play a cover drive' behaving differently depending on whether Virat Kohli or Steve Smith executes it -- each player's class implements the shot with the same name but their own technique.
Polymorphism lets you write generic code that works with any object exposing the expected interface, without needing to know the object's exact type in advance — a cornerstone of flexible, reusable object-oriented design.
Cricket analogy: Polymorphism lets a commentator say 'the bowler took a wicket' generically, without needing to know in advance whether it's a pacer's yorker or a spinner's googly -- any object exposing a 'take_wicket' interface works.
2. Syntax
class Shape:
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
for shape in [Circle(2), Square(3)]:
print(shape.area()) # same call, different behavior3. Explanation
In the example, both Circle and Square define an area() method, but each computes it differently. Calling shape.area() inside the loop works identically regardless of which subclass shape refers to — Python looks up area on the actual object's class at runtime and calls the matching implementation. This is sometimes called runtime (or dynamic) polymorphism.
Cricket analogy: Both a 'PaceBowler' and a 'SpinBowler' class define a deliver() method, but each computes the ball's trajectory differently; calling bowler.deliver() in the over-loop works identically regardless of which subclass is bowling -- runtime polymorphism resolved at call time.
Python also achieves polymorphism through operator overloading (e.g., + works differently for int, str, and list via each type's __add__), and through built-in functions like len() working on strings, lists, and dicts alike because each defines __len__.
Cricket analogy: Python's + working differently for int, str, and list mirrors how a scoreboard's 'total' operation works differently for runs, player names, and squad lists, each type defining its own __add__; similarly, len() works on a playing XI list because it defines __len__.
Python's polymorphism is largely driven by duck typing: "If it walks like a duck and quacks like a duck, it's treated as a duck." Python does not require objects to share a common base class to be used polymorphically — it only checks, at call time, whether the object has the method being called. This is different from strictly typed languages that require formal interface implementation.
Because Python is dynamically typed, calling a method that an object doesn't implement raises an AttributeError only at runtime, not at compile time. If Shape.area() is meant to be a required contract, using an Abstract Base Class (ABC) with @abstractmethod (from the abc module) is safer than just raise NotImplementedError, since ABCs prevent instantiating an incomplete subclass at all.
4. Example
class Shape:
def area(self):
raise NotImplementedError("Subclasses must implement area()")
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return round(3.14159 * self.radius ** 2, 2)
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
class Duck:
def area(self):
return "Ducks don't have area, but they can quack!"
shapes = [Circle(2), Square(3), Duck()]
for shape in shapes:
print(f"{type(shape).__name__}: {shape.area()}")5. Output
Circle: 12.57
Square: 9
Duck: Ducks don't have area, but they can quack!6. Key Takeaways
- Polymorphism lets the same method name behave differently depending on the object's class.
- Python resolves which method implementation to run at runtime, based on the object's actual type.
- Duck typing means Python cares whether an object supports a method, not its declared type or base class.
- Operator overloading (e.g., __add__, __len__) is a form of built-in polymorphism.
- For enforced contracts, prefer Abstract Base Classes over relying solely on NotImplementedError.
Practice what you learned
1. What does polymorphism allow different classes to do?
2. What is duck typing?
3. In the example, why does Duck().area() work even though Duck does not inherit from Shape?
4. Which built-in mechanism is an example of polymorphism via dunder methods?
5. Why might raise NotImplementedError in a base class be insufficient to truly enforce a contract?
Was this page helpful?
You May Also Like
Inheritance in Python
How subclasses reuse and extend behavior from parent classes, and how super() and MRO resolve method lookups.
Abstract Classes in Python
Using the abc module to define abstract base classes and abstract methods that force subclasses to implement required behavior.
Operator Overloading in Python
How to make Python's built-in operators (+, -, *, ==, <, etc.) work on your own classes by implementing the corresponding dunder methods.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics