Python Polymorphism
Master the art of writing flexible, extensible code with polymorphism
🎭 What is Polymorphism?
Polymorphism allows objects of different types to be treated as instances of the same type through a common interface. It's the ability of different classes to be used interchangeably, even though each class implements the same interface differently.
# Example of built-in polymorphism in Python
numbers = [1, 2, 3] # List
text = "Hello" # String
tuple_data = (1, 2) # Tuple
# len() works polymorphically with different types
print(len(numbers)) # Output: 3
print(len(text)) # Output: 5
print(len(tuple_data)) # Output: 2
# + operator works differently based on type
print(numbers + [4, 5]) # List concatenation: [1, 2, 3, 4, 5]
print(text + " World") # String concatenation: "Hello World"
print(tuple_data + (3,)) # Tuple concatenation: (1, 2, 3)
Understanding Polymorphism Theory
Polymorphism is one of the four pillars of object-oriented programming, alongside encapsulation, inheritance, and abstraction:
🧠 Core Concepts
Method Overriding
Child classes can provide specific implementations of methods defined in parent classes.
Duck Typing
Python's approach: "If it walks like a duck and quacks like a duck, it's a duck."
Interface Consistency
Different objects can be used interchangeably if they implement the same interface.
Runtime Binding
The specific method to call is determined at runtime based on the object's actual type.
🎯 Benefits of Polymorphism
- Code Reusability: Write code once, use with multiple types
- Flexibility: Easy to add new types without changing existing code
- Maintainability: Changes to implementations don't affect client code
- Extensibility: New classes can be added seamlessly
- Abstraction: Hide implementation details behind common interfaces
Method Overriding - Basic Polymorphism
The simplest form of polymorphism occurs when child classes override parent class methods:
# Base class
class Animal:
def __init__(self, name):
self.name = name
def make_sound(self):
return "Some generic animal sound"
def info(self):
return f"{self.name} is an animal"
# Child classes with overridden methods
class Dog(Animal):
def make_sound(self):
return "Woof! Woof!"
class Cat(Animal):
def make_sound(self):
return "Meow!"
class Cow(Animal):
def make_sound(self):
return "Moo!"
# Using polymorphism
animals = [
Dog("Buddy"),
Cat("Whiskers"),
Cow("Bessie")
]
# Same method call, different behaviors
for animal in animals:
print(f"{animal.name}: {animal.make_sound()}")
# Output:
# Buddy: Woof! Woof!
# Whiskers: Meow!
# Bessie: Moo!
💡 Key Points
-
Same Interface:
All animals have a
make_sound()method - Different Implementations: Each animal makes a different sound
- Runtime Decision: Python decides which method to call at runtime
- Transparent Usage: Client code doesn't need to know the specific animal type
Duck Typing - Python's Polymorphism
Python uses duck typing, where the type of an object is determined by its behavior rather than its class hierarchy:
# Different classes with same interface (no inheritance)
class Duck:
def fly(self):
return "Duck flying with wings"
def swim(self):
return "Duck swimming in water"
class Airplane:
def fly(self):
return "Airplane flying with engines"
class Fish:
def swim(self):
return "Fish swimming underwater"
class Boat:
def swim(self):
return "Boat floating on water"
# Functions that work with any object having the required method
def make_it_fly(flying_object):
return flying_object.fly()
def make_it_swim(swimming_object):
return swimming_object.swim()
# Duck typing in action
duck = Duck()
plane = Airplane()
fish = Fish()
boat = Boat()
# These work because objects have the required methods
print(make_it_fly(duck)) # Duck flying with wings
print(make_it_fly(plane)) # Airplane flying with engines
print(make_it_swim(duck)) # Duck swimming in water
print(make_it_swim(fish)) # Fish swimming underwater
print(make_it_swim(boat)) # Boat floating on water
# This would raise an AttributeError
# print(make_it_fly(fish)) # Fish doesn't have fly() method
🦆 Duck Typing Philosophy
"If it walks like a duck and it quacks like a duck, then it must be a duck."
In Python, you don't need to explicitly declare that objects implement a certain interface. If an object has the methods you need, you can use it regardless of its actual type.
Operator Overloading
Python allows you to define how operators work with your custom classes through special methods:
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Overload + operator"""
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
"""Overload - operator"""
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
"""Overload * operator for scalar multiplication"""
return Vector(self.x * scalar, self.y * scalar)
def __str__(self):
"""String representation"""
return f"Vector({self.x}, {self.y})"
def __eq__(self, other):
"""Overload == operator"""
return self.x == other.x and self.y == other.y
# Using overloaded operators
v1 = Vector(2, 3)
v2 = Vector(1, 4)
# Addition
v3 = v1 + v2
print(v3) # Vector(3, 7)
# Subtraction
v4 = v1 - v2
print(v4) # Vector(1, -1)
# Scalar multiplication
v5 = v1 * 3
print(v5) # Vector(6, 9)
# Equality comparison
print(v1 == v2) # False
print(v1 == Vector(2, 3)) # True
🔧 Common Magic Methods
Abstract Base Classes (ABC)
Use ABC to define formal interfaces and ensure consistent implementation across classes:
from abc import ABC, abstractmethod
# Abstract base class
class Shape(ABC):
@abstractmethod
def area(self):
"""Calculate the area of the shape"""
pass
@abstractmethod
def perimeter(self):
"""Calculate the perimeter of the shape"""
pass
# Concrete method (shared by all shapes)
def description(self):
return f"This is a {self.__class__.__name__} with area {self.area():.2f}"
# Concrete implementations
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):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
# Using the shapes polymorphically
shapes = [
Rectangle(5, 3),
Circle(4),
Rectangle(2, 8)
]
for shape in shapes:
print(shape.description())
print(f"Perimeter: {shape.perimeter():.2f}")
print("-" * 30)
# This would raise TypeError: Can't instantiate abstract class
# shape = Shape() # Error!
✅ ABC Benefits
- Interface Enforcement: Ensures all subclasses implement required methods
- Documentation: Clearly defines what methods a class must have
- Early Error Detection: Catches missing implementations at class definition time
- IDE Support: Better autocomplete and type checking
Modern Python: Protocols and Type Hints
Python 3.8+ introduced Protocols for structural typing, providing a more flexible approach to polymorphism:
from typing import Protocol
from typing import List
# Define a protocol (interface)
class Drawable(Protocol):
def draw(self) -> str:
"""Draw the object and return a string representation"""
...
# Classes that implicitly implement the protocol
class Circle:
def __init__(self, radius: float):
self.radius = radius
def draw(self) -> str:
return f"Drawing a circle with radius {self.radius}"
class Square:
def __init__(self, side: float):
self.side = side
def draw(self) -> str:
return f"Drawing a square with side {self.side}"
class Triangle:
def __init__(self, base: float, height: float):
self.base = base
self.height = height
def draw(self) -> str:
return f"Drawing a triangle with base {self.base} and height {self.height}"
# Function that works with any Drawable object
def render_shapes(shapes: List[Drawable]) -> None:
"""Render a list of drawable shapes"""
for shape in shapes:
print(shape.draw())
# Usage with type safety
shapes: List[Drawable] = [
Circle(5.0),
Square(3.0),
Triangle(4.0, 6.0)
]
render_shapes(shapes)
# Type checker will catch errors
# shapes.append("not a drawable") # Type error!
🚀 Protocol Advantages
- Structural Typing: No need for explicit inheritance
- Type Safety: Static type checkers can verify correctness
- Flexibility: Works with existing classes without modification
- Documentation: Clearly defines expected interfaces
Real-World Applications
Here are practical examples where polymorphism provides significant benefits:
🎮 Game Development
class GameObject:
def update(self):
pass
def render(self):
pass
class Player(GameObject):
def update(self):
# Handle player input and movement
pass
class Enemy(GameObject):
def update(self):
# AI behavior and movement
pass
class Bullet(GameObject):
def update(self):
# Move bullet forward
pass
# Game loop can handle all objects uniformly
game_objects = [Player(), Enemy(), Enemy(), Bullet()]
for obj in game_objects:
obj.update() # Each object behaves differently
obj.render() # Each object renders differently
💾 Data Processing
class DataProcessor:
def process(self, data):
pass
class CSVProcessor(DataProcessor):
def process(self, data):
# Process CSV data
return "Processed CSV data"
class JSONProcessor(DataProcessor):
def process(self, data):
# Process JSON data
return "Processed JSON data"
class XMLProcessor(DataProcessor):
def process(self, data):
# Process XML data
return "Processed XML data"
# Client code doesn't need to know the specific processor type
def handle_data(processor: DataProcessor, data):
return processor.process(data)
# Easy to add new processors without changing existing code
Polymorphism Best Practices
Design Principles
- Design interfaces before implementations
- Keep interfaces simple and focused
- Use composition over inheritance when possible
- Follow the Liskov Substitution Principle
Implementation Tips
- Use type hints for better code documentation
- Prefer Protocols over abstract base classes
- Handle edge cases in polymorphic methods
- Test all implementations thoroughly
Common Pitfalls
- Don't break the interface contract
- Avoid deep inheritance hierarchies
- Don't assume specific implementation details
- Handle missing methods gracefully
Performance Considerations
- Method resolution has slight overhead
- Use __slots__ for performance-critical classes
- Consider caching for expensive operations
- Profile polymorphic code in hot paths
🧠 Test Your Knowledge
Test your understanding of Python polymorphism: