- What is polymorphism ā the ability of different objects to respond to the same method call in their own way
- Method overriding ā child classes providing their own implementation
- Duck typing ā Python's approach to polymorphism without inheritance
- Operator overloading ā making operators work with your custom classes
- Real-world use ā practical examples you can use in your projects
What is Polymorphism?
The word polymorphism comes from Greek words meaning "many forms." In programming, it means that the same method name can behave differently depending on the object it's called on.
Think of a play() method. If you call it on a MusicPlayer object, it plays a song. If you call it on a VideoPlayer object, it plays a video. Same method name, different behavior.
Python supports polymorphism in three main ways:
- Method Overriding ā child classes override parent methods
- Duck Typing ā "if it walks like a duck and quacks like a duck..."
- Operator Overloading ā operators like + and * work differently for different types
š” Key concept: Polymorphism lets you write code that works with objects you haven't even created yet. You just need to know what methods they should have.
Method Overriding
Different Objects, Same Method, Different Behavior
Method overriding is the most common way polymorphism works in Python. When a child class defines a method that already exists in the parent class, it "overrides" the parent's version.
This means you can call the same method on different objects and get different results. This is what makes polymorphism powerful.
# Method Overriding ā The Foundation of Polymorphism
class Animal:
"""Base class for all animals"""
def __init__(self, name):
self.name = name
def make_sound(self):
"""Default sound ā to be overridden"""
return "Some animal sound"
def move(self):
"""Default movement ā to be overridden"""
return "Moves somehow"
class Dog(Animal):
"""Dog ā overrides make_sound and move"""
def make_sound(self):
return f"{self.name} says: Woof! Woof!"
def move(self):
return f"{self.name} runs on four legs"
class Cat(Animal):
"""Cat ā overrides make_sound and move"""
def make_sound(self):
return f"{self.name} says: Meow!"
def move(self):
return f"{self.name} walks silently"
class Bird(Animal):
"""Bird ā overrides make_sound and move"""
def make_sound(self):
return f"{self.name} says: Chirp! Chirp!"
def move(self):
return f"{self.name} flies through the air"
# ============================================================
# The Power of Polymorphism
# ============================================================
print("=" * 50)
print("METHOD OVERRIDING ā POLYMORPHISM")
print("=" * 50)
# Create different animals
animals = [
Dog("Rex"),
Cat("Whiskers"),
Bird("Tweety")
]
# Same method call ā different behavior
print("\nCalling make_sound() on each animal:")
for animal in animals:
print(f" {animal.make_sound()}")
print("\nCalling move() on each animal:")
for animal in animals:
print(f" {animal.move()}")
# ============================================================
# Polymorphism in Action
# ============================================================
print("\n" + "-" * 30)
print("POLYMORPHISM IN ACTION")
print("-" * 30)
def animal_parade(animals):
"""This function works with ANY animal ā past, present, future!"""
print("šµ Animal Parade!")
for animal in animals:
print(f" {animal.name}: {animal.move()} ā {animal.make_sound()}")
animal_parade(animals)
# ============================================================
# Why This Matters
# ============================================================
print("\n" + "-" * 30)
print("WHY POLYMORPHISM MATTERS")
print("-" * 30)
print("""
1. You can write functions that work with ANY animal class
2. You don't need to know what type of animal you'll get
3. New animals can be added without changing existing code
4. The code is more flexible and easier to maintain
The key insight: The animal_parade() function doesn't care
what type of animal it's working with. It just calls
make_sound() and move() and trusts each object to know
how to handle it.
""")
print("ā
This is polymorphism ā different objects responding")
print(" to the same method call in their own way.")
Method overriding key points:
- Same method name ā different behavior based on the object
- Child classes provide their own implementation ā of inherited methods
- Works with any subclass ā even ones not yet created
- Makes code flexible ā you can add new classes without changing existing code
Quick Check: What makes method overriding polymorphic? (Answer: The same method call produces different behavior depending on the object's type)
Duck Typing
If It Walks Like a Duck...
Duck typing is Python's informal approach to polymorphism. The idea is: "If it walks like a duck and quacks like a duck, then it's a duck."
In practice, this means you don't care about the type of an object. You only care about the methods it has. If an object has a quack() method, you can call it ā regardless of what class it belongs to.
This is different from method overriding because duck typing doesn't require inheritance. Any class can participate as long as it has the right methods.
# Duck Typing ā Polymorphism Without Inheritance
print("=" * 50)
print("DUCK TYPING ā POLYMORPHISM WITHOUT INHERITANCE")
print("=" * 50)
# These classes DON'T inherit from a common parent
class Duck:
def quack(self):
return "Quack! Quack!"
def fly(self):
return "Duck flies low"
class Dog:
def quack(self):
return "Woof! (I'm trying to quack like a duck!)"
def fly(self):
return "Dog runs (can't fly)"
class Robot:
def quack(self):
return "Beep! Boop! (Quack mode activated)"
def fly(self):
return "Robot hovers with propellers"
class Car:
def honk(self):
return "Beep! Beep!"
# No quack() method!
# ============================================================
# Duck Typing in Action
# ============================================================
def make_it_quack(thing):
"""
This function doesn't care what 'thing' is.
It just needs it to have a quack() method.
"""
return thing.quack()
def make_it_fly(thing):
"""
This function doesn't care what 'thing' is.
It just needs it to have a fly() method.
"""
return thing.fly()
# All these work because all have quack()
print("š¦ Making things quack:")
print(f" Duck: {make_it_quack(Duck())}")
print(f" Dog: {make_it_quack(Dog())}")
print(f" Robot: {make_it_quack(Robot())}")
print("\nāļø Making things fly:")
print(f" Duck: {make_it_fly(Duck())}")
print(f" Dog: {make_it_fly(Dog())}")
print(f" Robot: {make_it_fly(Robot())}")
# This would fail ā Car doesn't have quack()
# print(make_it_quack(Car())) # AttributeError!
# ============================================================
# The Flexibility of Duck Typing
# ============================================================
print("\n" + "-" * 30)
print("THE POWER OF DUCK TYPING")
print("-" * 30)
def describe(thing):
"""
This function works with ANY object that has the right methods.
It doesn't care about inheritance or type.
"""
description = []
# Check if it can do certain things
if hasattr(thing, 'quack'):
description.append(f"Can quack: {thing.quack()}")
if hasattr(thing, 'fly'):
description.append(f"Can fly: {thing.fly()}")
if hasattr(thing, 'honk'):
description.append(f"Can honk: {thing.honk()}")
return ", ".join(description) if description else "Nothing special"
print("Describing different objects:")
print(f" Duck: {describe(Duck())}")
print(f" Dog: {describe(Dog())}")
print(f" Robot: {describe(Robot())}")
print(f" Car: {describe(Car())}")
print("\n" + "-" * 30)
print("KEY INSIGHT:")
print("-" * 30)
print("ā
Duck typing focuses on WHAT an object can do, not WHAT it is.")
print("ā
No inheritance is required ā any class can participate.")
print("ā
This is extremely flexible and Pythonic.")
print("ā ļø The trade-off: errors are caught at runtime, not compile time.")
Duck typing key points:
- No inheritance needed ā any class can participate
- Focus on behavior ā what methods does the object have?
- Very flexible ā you can use any object that has the right methods
- Runtime checking ā errors are caught when you try to call a missing method
- Pythonic ā "it's easier to ask for forgiveness than permission"
Quick Check: What's the difference between duck typing and method overriding? (Answer: Duck typing doesn't require inheritance; any class with the right methods works)
Operator Overloading
Making Operators Work for Your Classes
Operator overloading is another form of polymorphism. It lets you define how operators like +, -, *, and == work with your custom classes.
Think about the + operator. It works differently with numbers (addition) vs strings (concatenation). That's polymorphism in action. You can create your own classes that respond to + in their own way.
# Operator Overloading ā Polymorphism with Operators
print("=" * 50)
print("OPERATOR OVERLOADING")
print("=" * 50)
# ============================================================
# Example 1: Vector Class
# ============================================================
class Vector:
"""A 2D vector that supports mathematical operations"""
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Overload + : vector addition"""
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return Vector(self.x + other, self.y + other)
def __sub__(self, other):
"""Overload - : vector subtraction"""
if isinstance(other, Vector):
return Vector(self.x - other.x, self.y - other.y)
return Vector(self.x - other, self.y - other)
def __mul__(self, other):
"""Overload * : scalar multiplication"""
if isinstance(other, (int, float)):
return Vector(self.x * other, self.y * other)
return Vector(self.x * other.x, self.y * other.y)
def __eq__(self, other):
"""Overload == : equality check"""
if isinstance(other, Vector):
return self.x == other.x and self.y == other.y
return False
def __repr__(self):
return f"Vector({self.x}, {self.y})"
# Using the overloaded operators
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"v1 + v2 = {v1 + v2}")
print(f"v1 - v2 = {v1 - v2}")
print(f"v1 * 3 = {v1 * 3}")
print(f"v1 == v2? {v1 == v2}")
print(f"v1 == Vector(3, 4)? {v1 == Vector(3, 4)}")
# ============================================================
# Example 2: Money Class
# ============================================================
class Money:
"""Money class with currency support"""
def __init__(self, amount, currency="USD"):
self.amount = amount
self.currency = currency
def __add__(self, other):
"""Add money (same currency required)"""
if isinstance(other, Money):
if self.currency != other.currency:
raise ValueError(f"Cannot add {self.currency} and {other.currency}")
return Money(self.amount + other.amount, self.currency)
return Money(self.amount + other, self.currency)
def __sub__(self, other):
"""Subtract money (same currency required)"""
if isinstance(other, Money):
if self.currency != other.currency:
raise ValueError(f"Cannot subtract {self.currency} and {other.currency}")
return Money(self.amount - other.amount, self.currency)
return Money(self.amount - other, self.currency)
def __mul__(self, factor):
"""Multiply money by a number"""
return Money(self.amount * factor, self.currency)
def __gt__(self, other):
"""Greater than comparison"""
if isinstance(other, Money):
if self.currency != other.currency:
raise ValueError(f"Cannot compare {self.currency} and {other.currency}")
return self.amount > other.amount
return self.amount > other
def __repr__(self):
return f"${self.amount:.2f} {self.currency}"
# Using money operations
m1 = Money(100, "USD")
m2 = Money(50, "USD")
print(f"\n{'-' * 30}")
print("MONEY OPERATIONS")
print("-" * 30)
print(f"m1 = {m1}")
print(f"m2 = {m2}")
print(f"m1 + m2 = {m1 + m2}")
print(f"m1 - m2 = {m1 - m2}")
print(f"m1 * 3 = {m1 * 3}")
print(f"m1 > m2? {m1 > m2}")
print(f"m2 > m1? {m2 > m1}")
# ============================================================
# Common Operator Methods
# ============================================================
print("\n" + "-" * 30)
print("COMMON OPERATOR METHODS")
print("-" * 30)
print("""
āāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Operator ā Special Method ā
āāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā + ā __add__(self, other) ā
ā - ā __sub__(self, other) ā
ā * ā __mul__(self, other) ā
ā / ā __truediv__(self, other) ā
ā // ā __floordiv__(self, other) ā
ā % ā __mod__(self, other) ā
ā ** ā __pow__(self, other) ā
ā < ā __lt__(self, other) ā
ā <= ā __le__(self, other) ā
ā == ā __eq__(self, other) ā
ā != ā __ne__(self, other) ā
ā > ā __gt__(self, other) ā
ā >= ā __ge__(self, other) ā
ā len() ā __len__(self) ā
ā str() ā __str__(self) ā
ā repr() ā __repr__(self) ā
ā [] ā __getitem__(self, key) ā
ā in ā __contains__(self, item) ā
ā with ā __enter__(self) and __exit__(self, ...) ā
āāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
""")
print("ā
Operator overloading makes your classes feel like built-in types.")
print("ā
It's a powerful form of polymorphism that makes code more intuitive.")
Operator overloading key points:
- Special methods ā double underscore methods like
__add__,__eq__ - Makes classes intuitive ā users can use familiar operators
- Polymorphism in action ā operators mean different things in different contexts
- Flexible ā you can define any behavior you want
Quick Check: What special method is used to overload the + operator? (Answer: __add__)
Real-World Example
Building a Shape System
# Real-World Example: Shape System with Polymorphism
import math
print("=" * 60)
print("SHAPE SYSTEM ā POLYMORPHISM IN ACTION")
print("=" * 60)
# ============================================================
# BASE CLASS ā Shape
# ============================================================
class Shape:
"""Base class for all shapes"""
def __init__(self, name):
self.name = name
def area(self):
"""Calculate area ā to be overridden"""
return 0
def perimeter(self):
"""Calculate perimeter ā to be overridden"""
return 0
def describe(self):
"""Describe the shape"""
return f"Shape: {self.name}, Area: {self.area():.2f}, Perimeter: {self.perimeter():.2f}"
# ============================================================
# CHILD CLASSES ā Override area() and perimeter()
# ============================================================
class Rectangle(Shape):
def __init__(self, width, height):
super().__init__("Rectangle")
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
class Circle(Shape):
def __init__(self, radius):
super().__init__("Circle")
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
def perimeter(self):
return 2 * math.pi * self.radius
class Triangle(Shape):
def __init__(self, a, b, c):
super().__init__("Triangle")
self.a = a
self.b = b
self.c = c
def area(self):
# Heron's formula
s = (self.a + self.b + self.c) / 2
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))
def perimeter(self):
return self.a + self.b + self.c
class Square(Rectangle):
def __init__(self, side):
super().__init__(side, side)
self.name = "Square"
# ============================================================
# POLYMORPHIC FUNCTIONS
# ============================================================
def print_shape_info(shape):
"""Works with ANY shape ā polymorphism in action"""
print(f" {shape.describe()}")
def compare_shapes(shape1, shape2):
"""Compare two shapes by area"""
if shape1.area() > shape2.area():
return f"{shape1.name} is bigger than {shape2.name}"
elif shape1.area() < shape2.area():
return f"{shape2.name} is bigger than {shape1.name}"
else:
return f"{shape1.name} and {shape2.name} have the same area"
def total_area(shapes):
"""Calculate total area of any list of shapes"""
return sum(shape.area() for shape in shapes)
# ============================================================
# DEMONSTRATION
# ============================================================
# Create shapes
shapes = [
Rectangle(5, 3),
Circle(4),
Triangle(3, 4, 5),
Square(6)
]
print("\n1. SHAPE DESCRIPTIONS")
for shape in shapes:
print_shape_info(shape)
print("\n2. COMPARING SHAPES")
print(f" {compare_shapes(shapes[0], shapes[1])}")
print(f" {compare_shapes(shapes[1], shapes[2])}")
print(f" {compare_shapes(shapes[2], shapes[3])}")
print(f"\n3. TOTAL AREA")
print(f" Total area of all shapes: {total_area(shapes):.2f}")
# ============================================================
# DUCK TYPING EXAMPLE ā Works with any object that has area()
# ============================================================
print("\n4. DUCK TYPING ā Any object with area() works")
class RandomShape:
"""Not related to Shape class, but has area()"""
def area(self):
return 42
def name(self):
return "Random"
def process_shape(shape):
"""Works with ANY object that has area()"""
if hasattr(shape, 'area'):
return f"Processing: area = {shape.area()}"
return "This object doesn't have an area"
print(f" {process_shape(RandomShape())}")
print(f" {process_shape(Circle(5))}")
# ============================================================
# POLYMORPHISM WITH BUILT-IN TYPES
# ============================================================
print("\n5. POLYMORPHISM WITH BUILT-IN TYPES")
print(f" len('hello'): {len('hello')}") # String
print(f" len([1, 2, 3]): {len([1, 2, 3])}") # List
print(f" len({1, 2, 3, 4}): {len({1, 2, 3, 4})}") # Set
print("\n" + "=" * 60)
print("KEY TAKEAWAYS:")
print("=" * 60)
print("ā
Method overriding: Each shape has its own area() and perimeter()")
print("ā
Duck typing: RandomShape works even without inheritance")
print("ā
Operator overloading: Built-in types use polymorphism with len()")
print("ā
The code is flexible and can work with any shape, now or in the future")
Real-world example key points:
- Method overriding ā each shape implements area() and perimeter() differently
- Duck typing ā RandomShape works with process_shape() without inheritance
- Built-in polymorphism ā len() works with strings, lists, sets, and more
- Flexible functions ā total_area() and compare_shapes() work with any shape
Quick Check: What makes the total_area() function polymorphic? (Answer: It works with any object that has an area() method, regardless of type)
Best Practices
Using Polymorphism Effectively
# Best Practices for Polymorphism
print("=" * 60)
print("BEST PRACTICES FOR POLYMORPHISM")
print("=" * 60)
# ============================================================
# 1. DESIGN FOR POLYMORPHISM FROM THE START
# ============================================================
print("\n1. DESIGN FOR POLYMORPHISM")
# ā
DO: Design a clean interface that all subclasses will implement
class DataProcessor:
"""Abstract interface for data processing"""
def process(self, data):
raise NotImplementedError("Subclasses must implement process()")
def validate(self, data):
"""Optional: provide a default implementation"""
return True
class CSVProcessor(DataProcessor):
def process(self, data):
return f"Processing CSV: {data}"
def validate(self, data):
return ".csv" in data
class JSONProcessor(DataProcessor):
def process(self, data):
return f"Processing JSON: {data}"
def validate(self, data):
return ".json" in data
def handle_data(processor, data):
"""Works with ANY processor"""
if processor.validate(data):
return processor.process(data)
return "Invalid data format"
print(f" CSV: {handle_data(CSVProcessor(), 'data.csv')}")
print(f" JSON: {handle_data(JSONProcessor(), 'data.json')}")
# ============================================================
# 2. USE DUCK TYPING WISELY
# ============================================================
print("\n2. USE DUCK TYPING WISELY")
# ā
DO: Use duck typing when behavior is more important than type
def describe_item(item):
"""Works with anything that has these methods"""
result = []
if hasattr(item, 'name'):
result.append(f"Name: {item.name}")
if hasattr(item, 'size'):
result.append(f"Size: {item.size}")
if hasattr(item, 'color'):
result.append(f"Color: {item.color}")
return ", ".join(result) if result else "No description available"
class Book:
name = "Python Book"
size = "Large"
color = "Blue"
class Pen:
name = "Fountain Pen"
color = "Black"
print(f" Book: {describe_item(Book())}")
print(f" Pen: {describe_item(Pen())}")
# ā DON'T: Use type checking when duck typing would work
class BadExample:
def process(self, data):
# Bad: checking the type directly
if isinstance(data, str):
return data.upper()
elif isinstance(data, list):
return [item.upper() for item in data]
else:
return str(data)
# This is less flexible and less Pythonic
# ============================================================
# 3. USE ABCs FOR FORMAL INTERFACES
# ============================================================
print("\n3. USE ABCs FOR FORMAL INTERFACES")
from abc import ABC, abstractmethod
class Renderable(ABC):
"""Formal interface for renderable objects"""
@abstractmethod
def render(self):
pass
class Image(Renderable):
def render(self):
return "Rendering image"
class Video(Renderable):
def render(self):
return "Rendering video"
def render_media(media):
"""Works with any Renderable"""
return media.render()
print(f" Image: {render_media(Image())}")
print(f" Video: {render_media(Video())}")
# ============================================================
# 4. BE CONSISTENT WITH SPECIAL METHODS
# ============================================================
print("\n4. BE CONSISTENT WITH SPECIAL METHODS")
# ā
DO: Make operator overloading consistent
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
def __add__(self, other):
if isinstance(other, Temperature):
return Temperature(self.celsius + other.celsius)
return Temperature(self.celsius + other)
def __sub__(self, other):
if isinstance(other, Temperature):
return Temperature(self.celsius - other.celsius)
return Temperature(self.celsius - other)
def __eq__(self, other):
if isinstance(other, Temperature):
return self.celsius == other.celsius
return False
def __repr__(self):
return f"{self.celsius}°C"
t1 = Temperature(25)
t2 = Temperature(10)
print(f" t1: {t1}, t2: {t2}")
print(f" t1 + t2 = {t1 + t2}")
print(f" t1 - t2 = {t1 - t2}")
print(f" t1 == t2? {t1 == t2}")
# ============================================================
# 5. SUMMARY
# ============================================================
print("\n" + "=" * 60)
print("POLYMORPHISM BEST PRACTICES SUMMARY")
print("=" * 60)
print("""
āāāāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā PRACTICE ā WHY IT MATTERS ā
āāāāāāāāāāāāāāāāāāāāāāā¼āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Design for it early ā Makes your code extensible from the start ā
ā Use duck typing ā More flexible, more Pythonic ā
ā Use ABCs for formal ā Clear contracts, better documentation ā
ā Be consistent ā Users expect intuitive behavior ā
ā Document interfaces ā Others know what to implement ā
ā Keep it simple ā Don't overcomplicate ā
āāāāāāāāāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
š REMEMBER:
⢠Polymorphism makes your code more flexible
⢠It allows you to extend functionality without changing existing code
⢠Python supports polymorphism in multiple ways
⢠"It's easier to ask for forgiveness than permission"
""")
Best practices summary:
- Design for polymorphism ā plan your interfaces from the start
- Use duck typing wisely ā it's flexible but can lead to runtime errors
- Use ABCs for formal contracts ā when you need to enforce implementation
- Be consistent ā operator overloading should behave as users expect
- Document your interfaces ā tell others what methods to implement
Quick Check: When should you use ABCs vs duck typing? (Answer: ABCs when you want to enforce a contract and document the interface; duck typing when flexibility is more important)
Try It Yourself
Experiment with polymorphism in the editor below.
POLYMORPHISM - PRACTICE
==================================================
1. METHOD OVERRIDING
Toyota: Engine starts with key!
Harley: Kick start, engine roars!
Eco: Silent motor starts!
2. DUCK TYPING
Name: Musician | Can play: Playing music
Name: Programmer | Can code: Writing code
Name: Chef | Can cook: Cooking food
3. OPERATOR OVERLOADING
p1 = Point(2, 3), p2 = Point(4, 5)
p1 + p2 = Point(6, 8)
p1 - p2 = Point(-2, -2)
p1 * 3 = Point(6, 9)
p1 == p2? False
==================================================
SUMMARY
You've Got It!
You now understand polymorphism in Python. You know how to use method overriding, duck typing, and operator overloading to make your code more flexible and reusable.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is polymorphism in Python?
What's the difference between polymorphism and inheritance?
Is operator overloading considered polymorphism?
When should I use duck typing vs inheritance-based polymorphism?
Is polymorphism the same as overloading?
How does polymorphism make code better?
Where to Go From Here
Now that you understand polymorphism in Python, check out these related topics:
Method Overriding
Learn more about overriding methods in Python.
Learn More āInheritance in Python
Understand how inheritance enables polymorphism.
Learn More āEncapsulation
Learn how encapsulation works with polymorphism.
Learn More ā