Python Polymorphism
Polymorphism means "many forms." In Python, it refers to the ability of different objects to respond to the same method call in different ways. Python supports polymorphism through duck typing, method overriding, and operator overloading.
Duck Typing
Python follows the principle: "If it walks like a duck and quacks like a duck, it's a duck." This means Python cares about what an object can do, not what type it is.
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
class Duck:
def speak(self):
return "Quack!"
# Polymorphic function - works with any object that has speak()
def animal_sound(animal):
print(animal.speak())
# No common parent class needed!
animals = [Dog(), Cat(), Duck()]
for animal in animals:
animal_sound(animal)
# Woof!
# Meow!
# Quack!Method Overriding
Child classes can provide their own implementation of parent methods:
class Shape:
def area(self):
return 0
def describe(self):
return f"{self.__class__.__name__}: area = {self.area():.2f}"
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self): # Override
import math
return math.pi * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self): # Override
return self.width * self.height
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self): # Override
return 0.5 * self.base * self.height
# Polymorphism through a common interface
shapes = [Circle(5), Rectangle(4, 6), Triangle(3, 8)]
for shape in shapes:
print(shape.describe())
# Circle: area = 78.54
# Rectangle: area = 24.00
# Triangle: area = 12.00
# Total area calculation - works regardless of shape type
total = sum(s.area() for s in shapes)
print(f"Total area: {total:.2f}")Operator Overloading
Define how operators work with your custom classes using magic methods:
class Money:
def __init__(self, amount, currency="USD"):
self.amount = amount
self.currency = currency
def __add__(self, other):
if self.currency != other.currency:
raise ValueError("Cannot add different currencies")
return Money(self.amount + other.amount, self.currency)
def __sub__(self, other):
if self.currency != other.currency:
raise ValueError("Cannot subtract different currencies")
return Money(self.amount - other.amount, self.currency)
def __mul__(self, factor):
return Money(self.amount * factor, self.currency)
def __eq__(self, other):
return self.amount == other.amount and self.currency == other.currency
def __lt__(self, other):
if self.currency != other.currency:
raise ValueError("Cannot compare different currencies")
return self.amount < other.amount
def __str__(self):
return f"${self.amount:.2f} {self.currency}"
def __repr__(self):
return f"Money({self.amount}, '{self.currency}')"
price = Money(29.99)
tax = Money(2.40)
total = price + tax
print(total) # $32.39 USD
discount = Money(5.00)
final = total - discount
print(final) # $27.39 USD
doubled = price * 2
print(doubled) # $59.98 USD
print(price == Money(29.99)) # True
print(price < total) # TruePolymorphism with Built-in Functions
Python built-ins use polymorphism extensively:
# len() works with many types
print(len("Python")) # 6 (string)
print(len([1, 2, 3])) # 3 (list)
print(len({"a": 1})) # 1 (dict)
# + operator works differently for different types
print(3 + 4) # 7 (addition)
print("Hello " + "World") # Hello World (concatenation)
print([1, 2] + [3, 4]) # [1, 2, 3, 4] (list concatenation)
# iter() and for loops
for item in [1, 2, 3]: # List iteration
pass
for char in "abc": # String iteration
pass
for key in {"a": 1}: # Dict iteration
pass
# Custom class with len and iteration
class Playlist:
def __init__(self, name, songs):
self.name = name
self.songs = songs
def __len__(self):
return len(self.songs)
def __iter__(self):
return iter(self.songs)
def __getitem__(self, index):
return self.songs[index]
playlist = Playlist("My Mix", ["Song A", "Song B", "Song C"])
print(len(playlist)) # 3
for song in playlist:
print(song)
print(playlist[0]) # Song AAbstract Classes
Use the abc module to define abstract classes that enforce method implementation:
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
"""Abstract base class for payment processors."""
@abstractmethod
def process_payment(self, amount):
"""Process a payment. Must be implemented by subclasses."""
pass
@abstractmethod
def refund(self, amount):
"""Process a refund. Must be implemented by subclasses."""
pass
def validate_amount(self, amount):
"""Concrete method shared by all subclasses."""
return amount > 0
class CreditCardProcessor(PaymentProcessor):
def __init__(self, card_number):
self.card_number = card_number
def process_payment(self, amount):
if self.validate_amount(amount):
return f"Charged ${amount:.2f} to card ending {self.card_number[-4:]}"
return "Invalid amount"
def refund(self, amount):
return f"Refunded ${amount:.2f} to card ending {self.card_number[-4:]}"
class PayPalProcessor(PaymentProcessor):
def __init__(self, email):
self.email = email
def process_payment(self, amount):
if self.validate_amount(amount):
return f"Charged ${amount:.2f} via PayPal ({self.email})"
return "Invalid amount"
def refund(self, amount):
return f"Refunded ${amount:.2f} via PayPal ({self.email})"
# Cannot instantiate abstract class
# processor = PaymentProcessor() # TypeError!
# Use concrete subclasses
processors = [
CreditCardProcessor("4111-1111-1111-1234"),
PayPalProcessor("alice@example.com"),
]
for processor in processors:
print(processor.process_payment(99.99))
print(processor.refund(25.00))
print()Protocol (Structural Typing)
Python 3.8+ supports Protocols for structural (duck) typing with type hints:
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str:
...
class Circle:
def draw(self) -> str:
return "Drawing circle"
class Square:
def draw(self) -> str:
return "Drawing square"
def render(shape: Drawable) -> None:
"""Accepts any object with a draw() method."""
print(shape.draw())
# No inheritance needed - just implement draw()
render(Circle()) # Drawing circle
render(Square()) # Drawing squareSummary
- Polymorphism allows different objects to respond to the same method call differently
- Duck typing: Python cares about what an object can do, not its type
- Method overriding: Child classes can provide their own implementation of parent methods
- Operator overloading: Define
__add__,__eq__,__lt__, etc. for custom operator behavior - Built-in functions like
len(),iter(),str()are polymorphic - Abstract classes (ABC) enforce that subclasses implement required methods
- Protocols enable structural typing with type hints
Next, we'll learn about encapsulation in Python.