Polymorphism in Python
Polymorphism is one of the four fundamental principles of Object-Oriented Programming (OOP). It allows objects of different classes to be treated as objects of a common superclass, enabling code to work with objects of multiple types through a unified interface.
The word “polymorphism” comes from Greek words meaning “many forms.”
Types of Polymorphism
1. Method Overriding (Runtime Polymorphism)
- Subclasses provide specific implementations of methods defined in their superclass
- The method to be called is determined at runtime based on the object’s actual type
class Animal:
def speak(self):
pass
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
class Bird(Animal):
def speak(self):
return "Tweet!"
# Polymorphic behavior
def animal_sound(animal):
return animal.speak()
# Different objects, same interface
dog = Dog()
cat = Cat()
bird = Bird()
print(animal_sound(dog)) # Woof!
print(animal_sound(cat)) # Meow!
print(animal_sound(bird)) # Tweet!
2. Method Overloading (Compile-time Polymorphism)
- Python doesn’t support traditional method overloading like Java/C++
- But we can achieve similar functionality using default parameters and variable arguments
class Calculator:
def add(self, a, b, c=None):
if c is None:
return a + b
else:
return a + b + c
def multiply(self, *args):
result = 1
for num in args:
result *= num
return result
calc = Calculator()
print(calc.add(5, 3)) # 8 (2 parameters)
print(calc.add(5, 3, 2)) # 10 (3 parameters)
print(calc.multiply(2, 3)) # 6
print(calc.multiply(2, 3, 4)) # 24
Operator Overloading
Python allows you to define how operators work with your custom objects:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Point(self.x - other.x, self.y - other.y)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __str__(self):
return f"Point({self.x}, {self.y})"
def __len__(self):
return int((self.x**2 + self.y**2)**0.5)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1 + p2) # Point(4, 6)
print(p2 - p1) # Point(2, 2)
print(p1 == p2) # False
print(len(p1)) # 2 (distance from origin)
Duck Typing
Python’s dynamic typing allows for “duck typing” - if it walks like a duck and quacks like a duck, it’s a duck:
class Duck:
def swim(self):
return "Duck swimming"
def quack(self):
return "Quack quack!"
class RubberDuck:
def swim(self):
return "Rubber duck floating"
def quack(self):
return "Squeak squeak!"
class Person:
def swim(self):
return "Person swimming"
def quack(self):
return "Person quacking!"
# Polymorphic function - works with any object that has swim and quack methods
def make_it_swim_and_quack(duck_like_object):
print(duck_like_object.swim())
print(duck_like_object.quack())
# All these work, even though they're different types
make_it_swim_and_quack(Duck())
make_it_swim_and_quack(RubberDuck())
make_it_swim_and_quack(Person())
Abstract Base Classes (ABCs)
ABCs provide a way to define interfaces and enforce polymorphism:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
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):
self.radius = radius
def area(self):
import math
return math.pi * self.radius ** 2
def perimeter(self):
import math
return 2 * math.pi * self.radius
# Polymorphic function
def print_shape_info(shape):
print(f"Area: {shape.area():.2f}")
print(f"Perimeter: {shape.perimeter():.2f}")
rect = Rectangle(5, 3)
circle = Circle(4)
print_shape_info(rect) # Works with Rectangle
print_shape_info(circle) # Works with Circle
Function Polymorphism
Functions can be polymorphic by accepting different types:
def process_data(data):
"""Polymorphic function that works with different data types"""
if isinstance(data, str):
return data.upper()
elif isinstance(data, list):
return [item.upper() if isinstance(item, str) else item for item in data]
elif isinstance(data, dict):
return {k: v.upper() if isinstance(v, str) else v for k, v in data.items()}
else:
return str(data)
# Same function, different types
print(process_data("hello")) # HELLO
print(process_data(["hello", "world"])) # ['HELLO', 'WORLD']
print(process_data({"greeting": "hello"})) # {'greeting': 'HELLO'}
print(process_data(42)) # 42
Built-in Polymorphism
Python’s built-in functions are polymorphic:
# len() works with different types
print(len("hello")) # 5 (string)
print(len([1, 2, 3])) # 3 (list)
print(len({"a": 1})) # 1 (dict)
# + operator works with different types
print("hello" + " world") # hello world (string concatenation)
print([1, 2] + [3, 4]) # [1, 2, 3, 4] (list concatenation)
print(5 + 3) # 8 (addition)
# print() works with any type
print("String")
print(42)
print([1, 2, 3])
print({"key": "value"})
Method Polymorphism with Inheritance
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
def get_salary(self):
return self.salary
def get_bonus(self):
return self.salary * 0.1
class Manager(Employee):
def __init__(self, name, salary, department):
super().__init__(name, salary)
self.department = department
def get_bonus(self):
return self.salary * 0.2 # Managers get higher bonus
class Developer(Employee):
def __init__(self, name, salary, programming_language):
super().__init__(name, salary)
self.programming_language = programming_language
def get_bonus(self):
return self.salary * 0.15 # Developers get medium bonus
# Polymorphic function
def calculate_total_compensation(employee):
return employee.get_salary() + employee.get_bonus()
# Different employee types
manager = Manager("Alice", 80000, "Engineering")
developer = Developer("Bob", 70000, "Python")
print(f"{manager.name}: ${calculate_total_compensation(manager)}")
print(f"{developer.name}: ${calculate_total_compensation(developer)}")
Interface Polymorphism
Using protocols (structural typing) in Python:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str:
...
class Circle:
def __init__(self, radius):
self.radius = radius
def draw(self):
return f"Drawing circle with radius {self.radius}"
class Square:
def __init__(self, side):
self.side = side
def draw(self):
return f"Drawing square with side {self.side}"
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def draw(self):
return f"Drawing triangle with base {self.base} and height {self.height}"
# Polymorphic function using protocol
def draw_shape(shape: Drawable):
print(shape.draw())
# All these work because they implement the draw method
draw_shape(Circle(5))
draw_shape(Square(4))
draw_shape(Triangle(3, 6))
Polymorphism with Magic Methods
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 __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __str__(self):
return f"Vector({self.x}, {self.y})"
def __len__(self):
return int((self.x**2 + self.y**2)**0.5)
class Matrix:
def __init__(self, data):
self.data = data
def __add__(self, other):
# Matrix addition logic
return Matrix([[self.data[i][j] + other.data[i][j]
for j in range(len(self.data[0]))]
for i in range(len(self.data))])
def __mul__(self, scalar):
return Matrix([[self.data[i][j] * scalar
for j in range(len(self.data[0]))]
for i in range(len(self.data))])
def __str__(self):
return str(self.data)
# Polymorphic operations
def multiply_by_two(obj):
return obj * 2
v = Vector(3, 4)
m = Matrix([[1, 2], [3, 4]])
print(multiply_by_two(v)) # Vector(6, 8)
print(multiply_by_two(m)) # [[2, 4], [6, 8]]
Summary Table
| Type of Polymorphism | Description | Example |
|---|---|---|
| Method Overriding | Subclasses override superclass methods | Dog.speak() vs Cat.speak() |
| Method Overloading | Same method with different parameters | add(a, b) vs add(a, b, c) |
| Operator Overloading | Custom behavior for operators | __add__, __sub__, __eq__ |
| Duck Typing | Interface-based polymorphism | Any object with required methods |
| Abstract Base Classes | Enforced interface contracts | @abstractmethod |
| Built-in Polymorphism | Python’s built-in polymorphic functions | len(), print(), + |
| Function Polymorphism | Functions that work with multiple types | process_data() |
Key Interview Points
- Polymorphism allows treating different objects through a common interface
- Method overriding is runtime polymorphism (most common in Python)
- Duck typing is Python’s approach to polymorphism
- Operator overloading uses magic methods like
__add__,__sub__ - Abstract Base Classes enforce interface contracts
- Built-in functions like
len(),print()are polymorphic - Protocols provide structural typing for polymorphism
- Polymorphism promotes code reusability and flexibility
- Python’s dynamic typing makes polymorphism natural and powerful
- Polymorphism is essential for writing flexible, maintainable code
Benefits of Polymorphism
- Code Reusability: Write once, use with many types
- Flexibility: Easy to extend with new types
- Maintainability: Changes in one place affect all implementations
- Readability: Code is more intuitive and expressive
- Scalability: Easy to add new functionality without changing existing code
Polymorphism is a powerful concept that makes Python code more flexible, maintainable, and elegant!
Interview angle
- “How does Python do polymorphism without interfaces?” — duck typing. Behaviour is determined by what an object supports at runtime, not by its declared type. Protocols make that statically checkable without requiring inheritance.
- “Does Python support method overloading?” — not by signature; a later definition simply replaces an earlier one. Achieve the same with default arguments,
*args/**kwargs, orfunctools.singledispatchfor genuine type-based dispatch.typing.overloaddeclares signatures for the type checker only and has no runtime effect. - “What does operator overloading actually do?” —
a + bcallsa.__add__(b), falling back tob.__radd__(a)if that returnsNotImplemented. ReturningNotImplementedrather than raising is what lets the other operand get a chance, and it’s the part people get wrong. - “EAFP or LBYL?” — Python prefers EAFP: attempt the operation and handle the exception, rather than checking types up front. It’s faster in the common case and avoids a check-then-use race.
hasattrchains are usually a sign you wanted a Protocol.