Polymorphism in Python: One Interface, Multiple Implementations

Polymorphism is a core object-oriented programming principle enabling objects of different types to be treated through a common interface, with each type providing its own implementation of shared methods. The term polymorphism derives from Greek meaning many forms, describing how a single method name or operator can behave differently depending on the object type it operates on. Python implements polymorphism through method overriding where child classes provide specialized implementations of parent methods, duck typing focusing on object behavior rather than inheritance relationships, operator overloading using magic methods defining custom behavior for operators like plus, minus, or equality, and polymorphic functions accepting different object types that implement required methods regardless of inheritance hierarchies.
This comprehensive guide explores method overriding polymorphism where child classes replace parent implementations enabling different classes responding to identical method calls with specialized behavior, duck typing philosophy if it walks like a duck and quacks like a duck then it's a duck emphasizing capabilities over types, operator overloading with magic methods like __add__, __len__, and __str__ defining custom behavior for built-in operators and functions, comparison operators using __eq__, __lt__, __gt__ enabling object comparisons, string representation methods __str__ for user-friendly output and __repr__ for developer debugging, container emulation with __getitem__, __setitem__, and __len__ making objects behave like lists or dictionaries, context manager protocol with __enter__ and __exit__ enabling with statement usage, practical polymorphic patterns designing functions accepting multiple types through shared interfaces, and best practices implementing intuitive operator behavior, avoiding operator overloading abuse, using duck typing for flexibility, and documenting expected interfaces. Whether you're designing class hierarchies with polymorphic methods, creating custom data structures supporting built-in operations, implementing mathematical objects with operator support, building frameworks accepting diverse object types, or writing flexible functions handling multiple implementations, mastering polymorphism provides essential tools for flexible, reusable code supporting multiple types through unified interfaces enabling clean, Pythonic APIs.
Method Overriding Polymorphism
Method overriding creates polymorphic behavior through inheritance where child classes provide specialized implementations of parent methods. When different classes override the same method name with class-specific implementations, a single function can process multiple object types polymorphically calling appropriate methods based on runtime object type. This inheritance-based polymorphism enables treating diverse objects uniformly through shared parent interfaces.
# Method Overriding Polymorphism
# === Basic polymorphism through inheritance ===
class Animal:
"""Base animal class."""
def __init__(self, name):
self.name = name
def speak(self):
pass # Abstract method
def move(self):
return f"{self.name} is moving"
class Dog(Animal):
"""Dog with specific speak implementation."""
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
"""Cat with specific speak implementation."""
def speak(self):
return f"{self.name} says Meow!"
class Bird(Animal):
"""Bird with specific speak implementation."""
def speak(self):
return f"{self.name} says Tweet!"
# Polymorphic function - same interface, different implementations
def make_animal_speak(animal):
"""Works with any Animal subclass."""
print(animal.speak())
# Create different animal types
dog = Dog("Buddy")
cat = Cat("Whiskers")
bird = Bird("Tweety")
# Same function, different behavior
make_animal_speak(dog) # Buddy says Woof!
make_animal_speak(cat) # Whiskers says Meow!
make_animal_speak(bird) # Tweety says Tweet!
# === Polymorphism with collections ===
animals = [Dog("Max"), Cat("Luna"), Bird("Chirpy"), Dog("Rex")]
# Process different types uniformly
for animal in animals:
print(animal.speak())
# Output:
# Max says Woof!
# Luna says Meow!
# Chirpy says Tweet!
# Rex says Woof!
# === Polymorphism in payment processing ===
class PaymentMethod:
"""Base payment method."""
def process_payment(self, amount):
pass
class CreditCard(PaymentMethod):
def __init__(self, card_number):
self.card_number = card_number
def process_payment(self, amount):
return f"Processing ${amount} via Credit Card ending in {self.card_number[-4:]}"
class PayPal(PaymentMethod):
def __init__(self, email):
self.email = email
def process_payment(self, amount):
return f"Processing ${amount} via PayPal account {self.email}"
class BankTransfer(PaymentMethod):
def __init__(self, account_number):
self.account_number = account_number
def process_payment(self, amount):
return f"Processing ${amount} via Bank Transfer to account {self.account_number}"
# Polymorphic payment processing
def checkout(payment_method, amount):
"""Process payment regardless of payment type."""
print(payment_method.process_payment(amount))
cc = CreditCard("1234567890123456")
pp = PayPal("[email protected]")
bt = BankTransfer("9876543210")
checkout(cc, 100) # Credit Card
checkout(pp, 50) # PayPal
checkout(bt, 200) # Bank Transfer
# === Polymorphism with shapes ===
class Shape:
"""Base shape class."""
def area(self):
pass
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):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
class Triangle(Shape):
def __init__(self, base, height, side1, side2, side3):
self.base = base
self.height = height
self.side1 = side1
self.side2 = side2
self.side3 = side3
def area(self):
return 0.5 * self.base * self.height
def perimeter(self):
return self.side1 + self.side2 + self.side3
# Polymorphic shape processing
def print_shape_info(shape):
"""Print info for any shape."""
print(f"Area: {shape.area():.2f}")
print(f"Perimeter: {shape.perimeter():.2f}")
shapes = [
Rectangle(10, 5),
Circle(7),
Triangle(8, 6, 5, 7, 8)
]
for shape in shapes:
print_shape_info(shape)
print()Duck Typing
Duck typing is Python's approach to polymorphism based on the philosophy if it walks like a duck and quacks like a duck, then it's a duck, focusing on object behavior rather than inheritance hierarchies. Objects need not share common parent classes to be used polymorphically; they only need to implement required methods with matching names. This dynamic typing enables flexible code accepting any object with appropriate capabilities regardless of type or inheritance relationships.
# Duck Typing
# === Basic duck typing ===
class Duck:
"""Real duck."""
def quack(self):
return "Quack!"
def fly(self):
return "Flying like a duck"
class Person:
"""Person imitating a duck."""
def quack(self):
return "I'm imitating a duck: Quack!"
def fly(self):
return "I can't fly, but I'm flapping my arms"
class Robot:
"""Robot that acts like a duck."""
def quack(self):
return "Mechanical quack sound"
def fly(self):
return "Flying with jet propulsion"
# Duck typing function - doesn't care about type, only behavior
def make_it_quack_and_fly(duck_like):
"""Accepts any object with quack() and fly() methods."""
print(duck_like.quack())
print(duck_like.fly())
# All work despite different types and no inheritance
duck = Duck()
person = Person()
robot = Robot()
make_it_quack_and_fly(duck)
make_it_quack_and_fly(person)
make_it_quack_and_fly(robot)
# === Duck typing with file-like objects ===
class StringWriter:
"""Acts like a file but writes to string."""
def __init__(self):
self.content = ""
def write(self, text):
self.content += text
def getvalue(self):
return self.content
class ListWriter:
"""Acts like a file but writes to list."""
def __init__(self):
self.lines = []
def write(self, text):
self.lines.append(text)
def getvalue(self):
return ''.join(self.lines)
# Function accepting any "file-like" object
def save_data(file_like, data):
"""Works with any object having write() method."""
for item in data:
file_like.write(f"{item}\n")
# Duck typing - both work despite being different types
string_writer = StringWriter()
list_writer = ListWriter()
data = ["Line 1", "Line 2", "Line 3"]
save_data(string_writer, data)
save_data(list_writer, data)
print(string_writer.getvalue())
print(list_writer.getvalue())
# === Duck typing with iterables ===
class Countdown:
"""Custom iterable counting down."""
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
# Works with any iterable (list, tuple, custom class)
def print_all(iterable):
"""Works with any object supporting iteration."""
for item in iterable:
print(item)
print_all([1, 2, 3]) # List
print_all((4, 5, 6)) # Tuple
print_all(Countdown(3)) # Custom iterable
# === Duck typing for database connections ===
class PostgresConnection:
def execute(self, query):
return f"PostgreSQL: Executing {query}"
def close(self):
return "PostgreSQL: Connection closed"
class MySQLConnection:
def execute(self, query):
return f"MySQL: Executing {query}"
def close(self):
return "MySQL: Connection closed"
class MockConnection:
"""For testing - no real database."""
def execute(self, query):
return f"Mock: Simulating {query}"
def close(self):
return "Mock: Connection closed"
# Function works with any connection-like object
def run_query(connection, query):
"""Accepts any object with execute() method."""
result = connection.execute(query)
print(result)
return result
# All work despite no inheritance relationship
pg = PostgresConnection()
mysql = MySQLConnection()
mock = MockConnection()
run_query(pg, "SELECT * FROM users")
run_query(mysql, "SELECT * FROM users")
run_query(mock, "SELECT * FROM users")
# === When duck typing fails ===
class NotADuck:
"""Doesn't have required methods."""
pass
try:
make_it_quack_and_fly(NotADuck())
except AttributeError as e:
print(f"Duck typing failed: {e}")
# Output: Duck typing failed: 'NotADuck' object has no attribute 'quack'
# === Duck typing vs inheritance ===
# Inheritance approach (rigid)
class Vehicle:
def start(self):
pass
class Car(Vehicle):
def start(self):
return "Car starting"
# Duck typing approach (flexible)
class Motorcycle: # No inheritance
def start(self):
return "Motorcycle starting"
class Bicycle: # No inheritance
def start(self):
return "Bicycle ready to ride"
def start_vehicle(vehicle):
"""Duck typing - accepts any object with start()."""
return vehicle.start()
# All work with duck typing
print(start_vehicle(Car()))
print(start_vehicle(Motorcycle()))
print(start_vehicle(Bicycle()))Operator Overloading with Magic Methods
Operator overloading defines custom behavior for built-in operators like plus, minus, multiply, and comparison operators through magic methods (special methods with double underscores). Magic methods like __add__ for addition, __eq__ for equality, and __len__ for length enable custom objects to behave like built-in types, supporting intuitive operations and seamless integration with Python's syntax. This polymorphism allows different classes to define operator meaning appropriately for their domain.
# Operator Overloading with Magic Methods
# === Arithmetic operators ===
class Vector:
"""2D vector with operator overloading."""
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
"""Define + operator."""
return Vector(self.x + other.x, self.y + other.y)
def __sub__(self, other):
"""Define - operator."""
return Vector(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
"""Define * operator (scalar multiplication)."""
return Vector(self.x * scalar, self.y * scalar)
def __str__(self):
"""String representation."""
return f"Vector({self.x}, {self.y})"
def __repr__(self):
"""Developer representation."""
return f"Vector({self.x}, {self.y})"
v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2) # Vector(4, 6)
print(v1 - v2) # Vector(2, 2)
print(v1 * 3) # Vector(9, 12)
# === Comparison operators ===
class Money:
"""Money with comparison operators."""
def __init__(self, amount):
self.amount = amount
def __eq__(self, other):
"""Define == operator."""
return self.amount == other.amount
def __ne__(self, other):
"""Define != operator."""
return self.amount != other.amount
def __lt__(self, other):
"""Define < operator."""
return self.amount < other.amount
def __le__(self, other):
"""Define <= operator."""
return self.amount <= other.amount
def __gt__(self, other):
"""Define > operator."""
return self.amount > other.amount
def __ge__(self, other):
"""Define >= operator."""
return self.amount >= other.amount
def __str__(self):
return f"${self.amount:.2f}"
m1 = Money(100)
m2 = Money(200)
m3 = Money(100)
print(m1 == m3) # True
print(m1 != m2) # True
print(m1 < m2) # True
print(m2 > m1) # True
# Can now sort Money objects
money_list = [Money(50), Money(200), Money(100)]
sorted_money = sorted(money_list)
for m in sorted_money:
print(m) # $50.00, $100.00, $200.00
# === Container emulation ===
class CustomList:
"""List-like object with operator overloading."""
def __init__(self):
self.items = []
def __len__(self):
"""Define len() function."""
return len(self.items)
def __getitem__(self, index):
"""Define [] operator for getting."""
return self.items[index]
def __setitem__(self, index, value):
"""Define [] operator for setting."""
self.items[index] = value
def __delitem__(self, index):
"""Define del operator."""
del self.items[index]
def __contains__(self, item):
"""Define 'in' operator."""
return item in self.items
def append(self, item):
self.items.append(item)
def __str__(self):
return str(self.items)
cl = CustomList()
cl.append(10)
cl.append(20)
cl.append(30)
print(len(cl)) # 3
print(cl[1]) # 20
cl[1] = 25
print(cl[1]) # 25
print(20 in cl) # False
print(25 in cl) # True
# === String representation ===
class Book:
"""Book with string representations."""
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __str__(self):
"""User-friendly representation."""
return f"{self.title} by {self.author}"
def __repr__(self):
"""Developer representation."""
return f"Book('{self.title}', '{self.author}', {self.pages})"
book = Book("Python Mastery", "John Doe", 350)
print(str(book)) # Python Mastery by John Doe
print(repr(book)) # Book('Python Mastery', 'John Doe', 350)
print(book) # Uses __str__ by default
# === Context manager protocol ===
class FileManager:
"""File manager with context manager support."""
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
"""Called when entering 'with' block."""
print(f"Opening {self.filename}")
self.file = open(self.filename, self.mode)
return self.file
def __exit__(self, exc_type, exc_value, traceback):
"""Called when exiting 'with' block."""
print(f"Closing {self.filename}")
if self.file:
self.file.close()
return False
# Use with 'with' statement
with FileManager('test.txt', 'w') as f:
f.write('Hello, World!')
# File automatically closed
# === Callable objects ===
class Multiplier:
"""Callable object."""
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
"""Make object callable like a function."""
return x * self.factor
times_three = Multiplier(3)
times_five = Multiplier(5)
print(times_three(10)) # 30
print(times_five(10)) # 50
# === Complete example: Complex number ===
class Complex:
"""Complex number with full operator support."""
def __init__(self, real, imag):
self.real = real
self.imag = imag
def __add__(self, other):
return Complex(self.real + other.real, self.imag + other.imag)
def __sub__(self, other):
return Complex(self.real - other.real, self.imag - other.imag)
def __mul__(self, other):
real = self.real * other.real - self.imag * other.imag
imag = self.real * other.imag + self.imag * other.real
return Complex(real, imag)
def __eq__(self, other):
return self.real == other.real and self.imag == other.imag
def __abs__(self):
return (self.real ** 2 + self.imag ** 2) ** 0.5
def __str__(self):
sign = '+' if self.imag >= 0 else ''
return f"{self.real}{sign}{self.imag}i"
def __repr__(self):
return f"Complex({self.real}, {self.imag})"
c1 = Complex(3, 4)
c2 = Complex(1, 2)
print(c1 + c2) # 4+6i
print(c1 - c2) # 2+2i
print(c1 * c2) # -5+10i
print(abs(c1)) # 5.0+ should add, == should compare equality. Non-intuitive behavior confuses users.Polymorphism Best Practices
- Design for interfaces, not implementations: Write functions accepting any object implementing required methods rather than specific types, maximizing flexibility through duck typing
- Use descriptive method names: Polymorphic methods should have clear names describing behavior.
speak(),process(),calculate()indicate expected actions - Document expected behavior: Use docstrings specifying what methods polymorphic functions expect. Document parameters, return values, and required object capabilities
- Keep operator overloading intuitive: Overloaded operators should behave like built-in types.
+should combine,==should compare,len()should return size - Return appropriate types from operators: Arithmetic operators should return new objects, not modify in place. Comparison operators should return booleans
- Implement
__repr__for debugging: Always implement__repr__()returning unambiguous string recreating object. Use__str__()for user-friendly output - Use NotImplemented for unsupported operations: Return
NotImplementedfrom magic methods when operation unsupported for given type, allowing Python to try reflected operation - Embrace duck typing over type checking: Avoid
isinstance()checks in polymorphic functions. Trust objects to have required methods; let AttributeError reveal missing methods - Prefer composition with polymorphism: Combine polymorphism with composition. Objects containing polymorphic components enable flexible designs without deep inheritance hierarchies
- Test polymorphic code thoroughly: Test functions with multiple implementing types ensuring correct behavior. Include edge cases and type mismatches verifying error handling
Conclusion
Polymorphism enables objects of different types to be treated through common interfaces with each type providing specialized implementations, fundamental to flexible object-oriented Python code. Method overriding polymorphism occurs through inheritance where child classes replace parent method implementations, enabling functions accepting parent types to process all children polymorphically calling appropriate overridden methods based on runtime object types. Duck typing focuses on object behavior rather than inheritance relationships following the philosophy if it walks like a duck and quacks like a duck then it's a duck, requiring only that objects implement required methods regardless of type or parent classes, enabling flexible code accepting any object with appropriate capabilities.
Operator overloading uses magic methods like __add__, __eq__, __len__, and __str__ defining custom behavior for built-in operators and functions, enabling custom objects to support intuitive operations like addition, comparison, length queries, and string conversion seamlessly integrating with Python syntax. Comparison operators through __eq__, __lt__, __gt__, and related methods enable object comparisons and sorting, string representation methods __str__ for user-friendly output and __repr__ for developer debugging provide appropriate string conversions, container emulation with __getitem__, __setitem__, and __len__ makes objects behave like lists or dictionaries, and context manager protocol with __enter__ and __exit__ enables with statement usage. Best practices emphasize designing for interfaces rather than implementations maximizing duck typing flexibility, using descriptive method names indicating expected behavior, documenting expected capabilities in docstrings, keeping operator overloading intuitive matching built-in type behavior, returning appropriate types from operators creating new objects rather than modifying in place, implementing __repr__ for debugging with unambiguous representations, returning NotImplemented for unsupported operations allowing reflected operations, embracing duck typing over type checking trusting object capabilities, preferring composition with polymorphism avoiding deep inheritance, and testing polymorphic code with multiple types verifying correct behavior. By mastering method overriding polymorphism through inheritance hierarchies, duck typing focusing on capabilities over types, operator overloading customizing built-in operations, magic methods integrating with Python syntax, and best practices ensuring intuitive flexible designs, you gain essential tools for writing reusable polymorphic code accepting diverse types through unified interfaces, supporting clean Pythonic APIs, and enabling flexible extensible applications from data processing to framework development.
$ share --platform
$ cat /comments/ (0)
$ cat /comments/
// No comments found. Be the first!


