Python Polymorphism: Complete Guide with Examples
Master Python polymorphism - the ability to take many forms. Learn method overriding, operator overloading, duck typing with practical examples.
Method Overriding
Same method, different behavior
Operator Overloading
Custom operator behavior
Duck Typing
If it walks like a duck...
30+ Examples
Practical polymorphism patterns
What is Polymorphism?
Polymorphism (from Greek: "poly" = many, "morph" = forms) allows objects of different classes to be treated as objects of a common super class. In Python, polymorphism enables the same interface to be used for different data types.
Types of Polymorphism
- Method Overriding: Same method name in parent/child
- Operator Overloading: Same operator for different types
- Duck Typing: Focus on behavior, not type
- Runtime Polymorphism: Method resolution at runtime
Key Benefits
- Code reusability and flexibility
- Simplified interface for different objects
- Extensible and maintainable code
- Dynamic method resolution
- Cleaner, more readable code
Real-world Analogy
Think of a "start" method. A Car's start() ignites the engine. A Computer's start() boots the OS. A Meeting's start() begins the discussion. Same method name, different implementations!
draw()
Circle.draw()
Draws a circleRectangle.draw()
Draws a rectangleTriangle.draw()
Draws a triangleSame draw() method, different shapes!
# Basic Polymorphism Example
class Animal:
def speak(self):
raise NotImplementedError("Subclass must implement abstract method")
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
class Cow(Animal):
def speak(self):
return "Moo!"
# Polymorphic behavior
animals = [Dog(), Cat(), Cow()]
print("=== Animal Sounds (Polymorphism) ===")
for animal in animals:
# Same method call, different behaviors
print(f"{animal.__class__.__name__}: {animal.speak()}")
# Output:
# Dog: Woof!
# Cat: Meow!
# Cow: Moo!
# Another example with shapes
class Shape:
def area(self):
raise NotImplementedError("Subclass must implement area()")
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return 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
class Triangle(Shape):
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
# Polymorphic collection of shapes
print("\n=== Shape Areas (Polymorphism) ===")
shapes = [
Rectangle(4, 5),
Circle(3),
Triangle(6, 4)
]
for shape in shapes:
# Same method call, different calculations
print(f"{shape.__class__.__name__} area: {shape.area():.2f}")
# Output:
# Rectangle area: 20.00
# Circle area: 28.27
# Triangle area: 12.00
Method Overriding
Method overriding occurs when a child class provides a specific implementation of a method that is already defined in its parent class. The overridden method in the child class has the same name and parameters.
# =============================================
# EXAMPLE 1: Basic Method Overriding
# =============================================
class Vehicle:
"""Base Vehicle class"""
def __init__(self, brand, model):
self.brand = brand
self.model = model
self.speed = 0
def start(self):
"""Start the vehicle"""
return f"{self.brand} {self.model} starting..."
def accelerate(self, increment):
"""Accelerate the vehicle"""
self.speed += increment
return f"{self.brand} {self.model} accelerating to {self.speed} km/h"
def stop(self):
"""Stop the vehicle"""
self.speed = 0
return f"{self.brand} {self.model} stopping"
def get_info(self):
"""Get vehicle information"""
return f"{self.brand} {self.model}"
class Car(Vehicle):
"""Car class overriding Vehicle methods"""
def __init__(self, brand, model, doors):
super().__init__(brand, model)
self.doors = doors
self.is_engine_on = False
# Overriding start method
def start(self):
"""Car-specific start implementation"""
self.is_engine_on = True
return f"🚗 {self.brand} {self.model}: Engine started with key ignition"
# Overriding accelerate method
def accelerate(self, increment):
"""Car-specific acceleration"""
if not self.is_engine_on:
return "Cannot accelerate. Engine is off!"
self.speed += increment
if self.speed > 120:
return f"⚠️ {self.brand} {self.model}: Speed limit exceeded ({self.speed} km/h)"
return f"🚗 {self.brand} {self.model}: Accelerating to {self.speed} km/h"
# Overriding get_info method
def get_info(self):
"""Extended info with car details"""
base_info = super().get_info() # Call parent method
return f"{base_info} ({self.doors} doors)"
class Bicycle(Vehicle):
"""Bicycle class overriding Vehicle methods"""
def __init__(self, brand, model, gear_count):
super().__init__(brand, model)
self.gear_count = gear_count
self.current_gear = 1
# Overriding start method
def start(self):
"""Bicycle-specific start implementation"""
return f"🚲 {self.brand} {self.model}: Started pedaling"
# Overriding accelerate method
def accelerate(self, increment):
"""Bicycle-specific acceleration"""
self.speed += increment
if self.speed > 30:
return f"🚲 {self.brand} {self.model}: Pedaling fast at {self.speed} km/h"
return f"🚲 {self.brand} {self.model}: Cruising at {self.speed} km/h"
# New method specific to Bicycle
def change_gear(self, gear):
"""Bicycle-specific method"""
if 1 <= gear <= self.gear_count:
self.current_gear = gear
return f"Changed to gear {gear}"
return "Invalid gear"
class Airplane(Vehicle):
"""Airplane class overriding Vehicle methods"""
def __init__(self, brand, model, wingspan):
super().__init__(brand, model)
self.wingspan = wingspan
self.altitude = 0
# Overriding start method
def start(self):
"""Airplane-specific start implementation"""
return f"✈️ {self.brand} {self.model}: Starting engines and taxiing"
# Overriding accelerate method
def accelerate(self, increment):
"""Airplane-specific acceleration"""
self.speed += increment
# Takeoff logic
if self.speed >= 250 and self.altitude == 0:
self.altitude = 1000
return f"✈️ {self.brand} {self.model}: Took off! Speed: {self.speed} km/h, Altitude: {self.altitude} m"
if self.altitude > 0:
return f"✈️ {self.brand} {self.model}: Flying at {self.speed} km/h, Altitude: {self.altitude} m"
return f"✈️ {self.brand} {self.model}: Taxiing at {self.speed} km/h"
# New method specific to Airplane
def climb(self, feet):
"""Airplane-specific method"""
self.altitude += feet
return f"Climbing to {self.altitude} feet"
print("=== Method Overriding Examples ===")
# Create different vehicles
car = Car("Toyota", "Camry", 4)
bicycle = Bicycle("Giant", "Escape 3", 21)
airplane = Airplane("Boeing", "747", 68)
# Polymorphic collection
vehicles = [car, bicycle, airplane]
print("\n=== Starting Vehicles ===")
for vehicle in vehicles:
# Same method call, different implementations
print(vehicle.start())
print("\n=== Accelerating Vehicles ===")
for vehicle in vehicles:
# Same method call, different behaviors
print(vehicle.accelerate(50))
print(vehicle.accelerate(100))
print("\n=== Getting Vehicle Info ===")
for vehicle in vehicles:
# Some override get_info, others use parent version
print(vehicle.get_info())
print("\n=== Vehicle-specific Methods ===")
# Calling methods specific to certain vehicle types
print(bicycle.change_gear(5)) # Only Bicycle has this
print(airplane.climb(5000)) # Only Airplane has this
# =============================================
# EXAMPLE 2: Banking System with Method Overriding
# =============================================
class BankAccount:
"""Base bank account class"""
def __init__(self, account_holder, balance=0):
self.account_holder = account_holder
self.balance = balance
def deposit(self, amount):
"""Deposit money"""
if amount > 0:
self.balance += amount
return f"Deposited ${amount:.2f}"
return "Invalid deposit amount"
def withdraw(self, amount):
"""Withdraw money - generic implementation"""
if amount > 0 and amount <= self.balance:
self.balance -= amount
return f"Withdrew ${amount:.2f}"
return "Insufficient funds or invalid amount"
def calculate_interest(self):
"""Calculate interest - to be overridden"""
return 0
def __str__(self):
return f"{self.account_holder}'s Account: ${self.balance:.2f}"
class SavingsAccount(BankAccount):
"""Savings account with interest"""
INTEREST_RATE = 0.03 # 3% annual
# Overriding calculate_interest
def calculate_interest(self):
"""Calculate interest for savings account"""
interest = self.balance * self.INTEREST_RATE
self.balance += interest
return f"Interest added: ${interest:.2f}"
# Overriding withdraw with restrictions
def withdraw(self, amount):
"""Savings account withdrawal with limit"""
if amount > 1000:
return "Cannot withdraw more than $1000 from savings"
return super().withdraw(amount)
class CheckingAccount(BankAccount):
"""Checking account with overdraft"""
OVERDRAFT_LIMIT = 500
# Overriding withdraw to allow overdraft
def withdraw(self, amount):
"""Checking account with overdraft protection"""
if amount > 0 and amount <= (self.balance + self.OVERDRAFT_LIMIT):
self.balance -= amount
if self.balance < 0:
return f"Withdrew ${amount:.2f} (Overdraft: ${-self.balance:.2f})"
return f"Withdrew ${amount:.2f}"
return f"Cannot withdraw ${amount:.2f}. Exceeds limit"
class FixedDepositAccount(BankAccount):
"""Fixed deposit with penalties for early withdrawal"""
def __init__(self, account_holder, balance, duration_months):
super().__init__(account_holder, balance)
self.duration_months = duration_months
self.months_passed = 0
# Overriding withdraw with penalty
def withdraw(self, amount):
"""Fixed deposit withdrawal with penalty"""
if self.months_passed < self.duration_months:
penalty = amount * 0.05 # 5% penalty
actual_withdrawal = amount - penalty
self.balance -= amount
return f"Withdrew ${amount:.2f} (Penalty: ${penalty:.2f}, Received: ${actual_withdrawal:.2f})"
return super().withdraw(amount)
def pass_month(self):
"""Simulate month passing"""
self.months_passed += 1
print("\n" + "="*60)
print("=== Banking System with Method Overriding ===")
print("="*60)
# Create different accounts
savings = SavingsAccount("Alice", 5000)
checking = CheckingAccount("Bob", 2000)
fixed_deposit = FixedDepositAccount("Charlie", 10000, 12)
accounts = [savings, checking, fixed_deposit]
# Demonstrate polymorphic behavior
print("\n=== Initial Account Status ===")
for account in accounts:
print(account)
print("\n=== Depositing $1000 ===")
for account in accounts:
print(f"{account.account_holder}: {account.deposit(1000)}")
print("\n=== Withdrawing $1500 (Different Behaviors) ===")
for account in accounts:
result = account.withdraw(1500)
print(f"{account.account_holder}: {result}")
print("\n=== Calculating Interest (Savings Only) ===")
for account in accounts:
if hasattr(account, 'calculate_interest'):
result = account.calculate_interest()
print(f"{account.account_holder}: {result}")
print("\n=== Trying Overdraft (Checking Only) ===")
print(f"Bob: {checking.withdraw(3000)}") # Should use overdraft
print("\n=== Early Withdrawal Penalty (Fixed Deposit) ===")
print(f"Charlie: {fixed_deposit.withdraw(2000)}") # Should apply penalty
print("\n=== Final Account Status ===")
for account in accounts:
print(account)
# =============================================
# EXAMPLE 3: Employee System with Method Overriding
# =============================================
class Employee:
"""Base employee class"""
def __init__(self, name, employee_id):
self.name = name
self.employee_id = employee_id
def calculate_salary(self, hours_worked):
"""Calculate salary - base implementation"""
return hours_worked * 20 # $20 per hour default
def get_role_description(self):
"""Get role description"""
return "Generic Employee"
def work(self):
"""Perform work"""
return f"{self.name} is working"
class Developer(Employee):
"""Developer employee"""
def calculate_salary(self, hours_worked):
"""Developer-specific salary calculation"""
base_salary = hours_worked * 50 # $50 per hour
overtime = max(0, hours_worked - 160) * 75 # $75 overtime
return base_salary + overtime
def get_role_description(self):
"""Developer role description"""
return "Software Developer"
def work(self):
"""Developer work"""
return f"{self.name} is writing code"
class Manager(Employee):
"""Manager employee"""
def __init__(self, name, employee_id, team_size):
super().__init__(name, employee_id)
self.team_size = team_size
def calculate_salary(self, hours_worked):
"""Manager-specific salary calculation"""
base_salary = 5000 # Fixed monthly
team_bonus = self.team_size * 100
return base_salary + team_bonus
def get_role_description(self):
"""Manager role description"""
return f"Manager of {self.team_size} people"
def work(self):
"""Manager work"""
return f"{self.name} is managing the team"
class Salesperson(Employee):
"""Sales employee"""
def __init__(self, name, employee_id, commission_rate):
super().__init__(name, employee_id)
self.commission_rate = commission_rate
self.sales = 0
def calculate_salary(self, hours_worked):
"""Salesperson salary with commission"""
base_salary = hours_worked * 30
commission = self.sales * self.commission_rate
return base_salary + commission
def make_sale(self, amount):
"""Record a sale"""
self.sales += amount
return f"Sale recorded: ${amount}"
def get_role_description(self):
"""Salesperson role description"""
return f"Salesperson ({self.commission_rate*100}% commission)"
def work(self):
"""Salesperson work"""
return f"{self.name} is making sales calls"
print("\n" + "="*60)
print("=== Employee System with Method Overriding ===")
print("="*60)
# Create employees
dev = Developer("Alice", "DEV001")
manager = Manager("Bob", "MGR001", 5)
sales = Salesperson("Charlie", "SAL001", 0.1)
employees = [dev, manager, sales]
# Record sales for salesperson
sales.make_sale(5000)
sales.make_sale(3000)
print("\n=== Employee Information ===")
for emp in employees:
print(f"{emp.name} ({emp.employee_id}): {emp.get_role_description()}")
print("\n=== What Employees Do ===")
for emp in employees:
print(emp.work())
print("\n=== Salary Calculation (160 hours) ===")
for emp in employees:
salary = emp.calculate_salary(160)
print(f"{emp.name}: ${salary:.2f}")
print("\n=== Special Methods ===")
# Salesperson-specific method
if hasattr(sales, 'make_sale'):
print(sales.make_sale(2000))
print(f"Updated salary: ${sales.calculate_salary(160):.2f}")
super() to call the parent class method from the overridden method in the child class. This allows you to extend parent functionality rather than completely replace it.
Operator Overloading
Operator overloading allows the same operator to have different meanings based on the context. Python uses special methods (dunder methods) that begin and end with double underscores to implement operator overloading.
| Operator | Method | Purpose | Example |
|---|---|---|---|
| + | __add__(self, other) |
Addition | obj1 + obj2 |
| - | __sub__(self, other) |
Subtraction | obj1 - obj2 |
| * | __mul__(self, other) |
Multiplication | obj1 * obj2 |
| / | __truediv__(self, other) |
Division | obj1 / obj2 |
| == | __eq__(self, other) |
Equality | obj1 == obj2 |
| < | __lt__(self, other) |
Less than | obj1 < obj2 |
| str() | __str__(self) |
String representation | print(obj) |
| len() | __len__(self) |
Length | len(obj) |
# =============================================
# EXAMPLE 1: Vector Math with Operator Overloading
# =============================================
class Vector:
"""Vector class demonstrating operator overloading"""
def __init__(self, x, y, z=0):
self.x = x
self.y = y
self.z = z
# Addition: vector + vector
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x,
self.y + other.y,
self.z + other.z)
raise TypeError("Can only add Vector to Vector")
# Subtraction: vector - vector
def __sub__(self, other):
if isinstance(other, Vector):
return Vector(self.x - other.x,
self.y - other.y,
self.z - other.z)
raise TypeError("Can only subtract Vector from Vector")
# Multiplication: vector * scalar OR scalar * vector
def __mul__(self, other):
if isinstance(other, (int, float)):
return Vector(self.x * other,
self.y * other,
self.z * other)
raise TypeError("Can only multiply Vector by scalar")
# Reverse multiplication: scalar * vector
def __rmul__(self, other):
return self.__mul__(other)
# Division: vector / scalar
def __truediv__(self, other):
if isinstance(other, (int, float)):
if other == 0:
raise ZeroDivisionError("Cannot divide vector by zero")
return Vector(self.x / other,
self.y / other,
self.z / other)
raise TypeError("Can only divide Vector by scalar")
# Dot product: vector @ vector
def __matmul__(self, other):
if isinstance(other, Vector):
return (self.x * other.x +
self.y * other.y +
self.z * other.z)
raise TypeError("Can only compute dot product with Vector")
# Equality: vector == vector
def __eq__(self, other):
if isinstance(other, Vector):
return (self.x == other.x and
self.y == other.y and
self.z == other.z)
return False
# Less than: vector < vector (compare magnitude)
def __lt__(self, other):
if isinstance(other, Vector):
return self.magnitude() < other.magnitude()
raise TypeError("Can only compare Vector with Vector")
# Greater than: vector > vector
def __gt__(self, other):
if isinstance(other, Vector):
return self.magnitude() > other.magnitude()
raise TypeError("Can only compare Vector with Vector")
# String representation: str(vector)
def __str__(self):
return f"Vector({self.x}, {self.y}, {self.z})"
# Representation: repr(vector)
def __repr__(self):
return f"Vector({self.x}, {self.y}, {self.z})"
# Length: len(vector) - returns dimension
def __len__(self):
return 3 # 3D vector
# Contains: value in vector
def __contains__(self, item):
return item in (self.x, self.y, self.z)
# Get item: vector[index]
def __getitem__(self, index):
if index == 0:
return self.x
elif index == 1:
return self.y
elif index == 2:
return self.z
raise IndexError("Vector index out of range")
# Set item: vector[index] = value
def __setitem__(self, index, value):
if index == 0:
self.x = value
elif index == 1:
self.y = value
elif index == 2:
self.z = value
else:
raise IndexError("Vector index out of range")
# Negation: -vector
def __neg__(self):
return Vector(-self.x, -self.y, -self.z)
# Absolute value: abs(vector) - returns magnitude
def __abs__(self):
return self.magnitude()
# Helper method
def magnitude(self):
"""Calculate vector magnitude"""
import math
return math.sqrt(self.x**2 + self.y**2 + self.z**2)
def normalize(self):
"""Return normalized vector"""
mag = self.magnitude()
if mag == 0:
return Vector(0, 0, 0)
return self / mag
print("=== Vector Operator Overloading ===")
v1 = Vector(1, 2, 3)
v2 = Vector(4, 5, 6)
print(f"v1 = {v1}")
print(f"v2 = {v2}")
print(f"\n=== Arithmetic Operations ===")
print(f"v1 + v2 = {v1 + v2}")
print(f"v2 - v1 = {v2 - v1}")
print(f"v1 * 2 = {v1 * 2}")
print(f"3 * v1 = {3 * v1}") # Uses __rmul__
print(f"v1 / 2 = {v1 / 2}")
print(f"-v1 = {-v1}")
print(f"abs(v1) = {abs(v1):.2f}")
print(f"\n=== Comparison Operations ===")
print(f"v1 == v2: {v1 == v2}")
print(f"v1 < v2: {v1 < v2}")
print(f"v1 > v2: {v1 > v2}")
print(f"\n=== Dot Product ===")
print(f"v1 @ v2 = {v1 @ v2}")
print(f"\n=== Other Operations ===")
print(f"len(v1) = {len(v1)}")
print(f"2 in v1: {2 in v1}")
print(f"v1[0] = {v1[0]}")
print(f"str(v1) = {str(v1)}")
print(f"repr(v1) = {repr(v1)}")
print(f"\n=== Vector Methods ===")
print(f"v1.magnitude() = {v1.magnitude():.2f}")
print(f"v1.normalize() = {v1.normalize()}")
# =============================================
# EXAMPLE 2: Complex Numbers with Operator Overloading
# =============================================
class ComplexNumber:
"""Complex number with operator overloading"""
def __init__(self, real, imag):
self.real = real
self.imag = imag
def __add__(self, other):
if isinstance(other, ComplexNumber):
return ComplexNumber(self.real + other.real,
self.imag + other.imag)
elif isinstance(other, (int, float)):
return ComplexNumber(self.real + other, self.imag)
raise TypeError("Unsupported operand type")
def __sub__(self, other):
if isinstance(other, ComplexNumber):
return ComplexNumber(self.real - other.real,
self.imag - other.imag)
elif isinstance(other, (int, float)):
return ComplexNumber(self.real - other, self.imag)
raise TypeError("Unsupported operand type")
def __mul__(self, other):
if isinstance(other, ComplexNumber):
# (a+bi)(c+di) = (ac-bd) + (ad+bc)i
real = self.real * other.real - self.imag * other.imag
imag = self.real * other.imag + self.imag * other.real
return ComplexNumber(real, imag)
elif isinstance(other, (int, float)):
return ComplexNumber(self.real * other, self.imag * other)
raise TypeError("Unsupported operand type")
def __truediv__(self, other):
if isinstance(other, ComplexNumber):
# (a+bi)/(c+di) = ((ac+bd)/(c²+d²)) + ((bc-ad)/(c²+d²))i
denominator = other.real**2 + other.imag**2
if denominator == 0:
raise ZeroDivisionError("Division by zero complex number")
real = (self.real * other.real + self.imag * other.imag) / denominator
imag = (self.imag * other.real - self.real * other.imag) / denominator
return ComplexNumber(real, imag)
elif isinstance(other, (int, float)):
if other == 0:
raise ZeroDivisionError("Division by zero")
return ComplexNumber(self.real / other, self.imag / other)
raise TypeError("Unsupported operand type")
def __eq__(self, other):
if isinstance(other, ComplexNumber):
return self.real == other.real and self.imag == other.imag
return False
def __str__(self):
if self.imag >= 0:
return f"{self.real} + {self.imag}i"
else:
return f"{self.real} - {-self.imag}i"
def __repr__(self):
return f"ComplexNumber({self.real}, {self.imag})"
def conjugate(self):
"""Return complex conjugate"""
return ComplexNumber(self.real, -self.imag)
def magnitude(self):
"""Return magnitude"""
import math
return math.sqrt(self.real**2 + self.imag**2)
print("\n" + "="*60)
print("=== Complex Number Operator Overloading ===")
print("="*60)
c1 = ComplexNumber(3, 4)
c2 = ComplexNumber(1, 2)
print(f"c1 = {c1}")
print(f"c2 = {c2}")
print(f"\nc1 + c2 = {c1 + c2}")
print(f"c1 - c2 = {c1 - c2}")
print(f"c1 * c2 = {c1 * c2}")
print(f"c1 / c2 = {c1 / c2}")
print(f"c1 == c2: {c1 == c2}")
print(f"c1.conjugate() = {c1.conjugate()}")
print(f"c1.magnitude() = {c1.magnitude():.2f}")
# =============================================
# EXAMPLE 3: Matrix Operator Overloading
# =============================================
class Matrix:
"""Simple matrix class with operator overloading"""
def __init__(self, data):
if not all(len(row) == len(data[0]) for row in data):
raise ValueError("All rows must have same length")
self.data = data
self.rows = len(data)
self.cols = len(data[0])
def __add__(self, other):
if not isinstance(other, Matrix):
raise TypeError("Can only add Matrix to Matrix")
if self.rows != other.rows or self.cols != other.cols:
raise ValueError("Matrices must have same dimensions")
result = []
for i in range(self.rows):
row = []
for j in range(self.cols):
row.append(self.data[i][j] + other.data[i][j])
result.append(row)
return Matrix(result)
def __sub__(self, other):
if not isinstance(other, Matrix):
raise TypeError("Can only subtract Matrix from Matrix")
if self.rows != other.rows or self.cols != other.cols:
raise ValueError("Matrices must have same dimensions")
result = []
for i in range(self.rows):
row = []
for j in range(self.cols):
row.append(self.data[i][j] - other.data[i][j])
result.append(row)
return Matrix(result)
def __mul__(self, other):
if isinstance(other, (int, float)):
# Scalar multiplication
result = []
for i in range(self.rows):
row = []
for j in range(self.cols):
row.append(self.data[i][j] * other)
result.append(row)
return Matrix(result)
elif isinstance(other, Matrix):
# Matrix multiplication
if self.cols != other.rows:
raise ValueError("Number of columns in first matrix must equal number of rows in second")
result = []
for i in range(self.rows):
row = []
for j in range(other.cols):
total = 0
for k in range(self.cols):
total += self.data[i][k] * other.data[k][j]
row.append(total)
result.append(row)
return Matrix(result)
raise TypeError("Unsupported operand type")
def __rmul__(self, other):
# Scalar multiplication from left
return self.__mul__(other)
def __matmul__(self, other):
# Matrix multiplication using @ operator
return self.__mul__(other)
def __str__(self):
rows = []
for row in self.data:
rows.append(" ".join(f"{val:6.1f}" for val in row))
return "[\n" + "\n".join(rows) + "\n]"
def __repr__(self):
return f"Matrix({self.data})"
def transpose(self):
"""Return transposed matrix"""
result = []
for j in range(self.cols):
row = []
for i in range(self.rows):
row.append(self.data[i][j])
result.append(row)
return Matrix(result)
def __eq__(self, other):
if not isinstance(other, Matrix):
return False
if self.rows != other.rows or self.cols != other.cols:
return False
for i in range(self.rows):
for j in range(self.cols):
if self.data[i][j] != other.data[i][j]:
return False
return True
print("\n" + "="*60)
print("=== Matrix Operator Overloading ===")
print("="*60)
m1 = Matrix([[1, 2], [3, 4]])
m2 = Matrix([[5, 6], [7, 8]])
print(f"m1 = \n{m1}")
print(f"\nm2 = \n{m2}")
print(f"\nm1 + m2 = \n{m1 + m2}")
print(f"\nm1 - m2 = \n{m1 - m2}")
print(f"\nm1 * 2 = \n{m1 * 2}")
print(f"\nm1 * m2 = \n{m1 * m2}")
print(f"\nm1 @ m2 = \n{m1 @ m2}") # Same as multiplication
print(f"\nm1.transpose() = \n{m1.transpose()}")
print(f"\nm1 == m2: {m1 == m2}")
Duck Typing
Duck typing is a programming concept where the type or class of an object is determined by its behavior (methods and properties) rather than its inheritance. "If it walks like a duck and quacks like a duck, it must be a duck."
Duck Typing Principle
In Python, we don't care about what an object is (its type/class), we care about what an object does (its methods and behavior). If an object has the required methods, it can be used regardless of its actual type.
# =============================================
# EXAMPLE 1: Classic Duck Typing Example
# =============================================
class Duck:
def quack(self):
return "Quack!"
def fly(self):
return "Flying high!"
def walk(self):
return "Waddling like a duck"
class Person:
def quack(self):
return "I'm quacking like a duck!"
def fly(self):
return "I'm flapping my arms!"
def walk(self):
return "Walking normally"
class Dog:
def bark(self):
return "Woof!"
def run(self):
return "Running fast"
# No quack, fly, or walk methods
class ToyDuck:
def quack(self):
return "Squeak squeak!"
# No fly or walk methods
def duck_test(obj):
"""Duck typing test - if it quacks like a duck..."""
try:
print(f"Testing {obj.__class__.__name__}:")
print(f" Can quack: {obj.quack()}")
print(f" Can fly: {obj.fly()}")
print(f" Can walk: {obj.walk()}")
return True
except AttributeError as e:
print(f" Not a duck! Missing: {e}")
return False
print("=== Classic Duck Typing ===")
duck = Duck()
person = Person()
dog = Dog()
toy = ToyDuck()
objects = [duck, person, dog, toy]
for obj in objects:
is_duck = duck_test(obj)
print(f" Is it a duck? {'Yes!' if is_duck else 'No'}\n")
# =============================================
# EXAMPLE 2: File-like Objects with Duck Typing
# =============================================
class StringIO:
"""String-based file-like object"""
def __init__(self, initial_string=""):
self.content = initial_string
self.position = 0
def read(self, size=-1):
"""Read from string"""
if size == -1:
result = self.content[self.position:]
self.position = len(self.content)
else:
result = self.content[self.position:self.position + size]
self.position += len(result)
return result
def write(self, string):
"""Write to string"""
self.content += string
return len(string)
def seek(self, position):
"""Seek to position"""
if 0 <= position <= len(self.content):
self.position = position
else:
raise ValueError("Invalid position")
def tell(self):
"""Get current position"""
return self.position
def close(self):
"""Close the stream"""
self.content = ""
self.position = 0
class NetworkStream:
"""Network-based file-like object"""
def __init__(self, server, port):
self.server = server
self.port = port
self.buffer = ""
self.connected = False
def connect(self):
"""Connect to server"""
self.connected = True
self.buffer = f"Connected to {self.server}:{self.port}"
def read(self, size=-1):
"""Read from network"""
if not self.connected:
raise ConnectionError("Not connected")
# Simulate network reading
if "HTTP" in self.buffer:
response = "HTTP/1.1 200 OK\nContent-Type: text/html\n\nHello World"
else:
response = f"Data from {self.server}"
if size == -1:
return response
return response[:size]
def write(self, data):
"""Write to network"""
if not self.connected:
raise ConnectionError("Not connected")
self.buffer = data
return len(data)
def close(self):
"""Close connection"""
self.connected = False
self.buffer = ""
def process_file_like_object(file_obj, data_to_write=""):
"""
Process any file-like object using duck typing
Doesn't care about actual type, only about methods
"""
print(f"\nProcessing {file_obj.__class__.__name__}:")
# Write if object supports it
if hasattr(file_obj, 'write'):
bytes_written = file_obj.write(data_to_write)
print(f" Written {bytes_written} bytes")
# Read if object supports it
if hasattr(file_obj, 'read'):
content = file_obj.read()
print(f" Read content: {content[:50]}...")
# Close if object supports it
if hasattr(file_obj, 'close'):
file_obj.close()
print(f" Closed successfully")
print("\n" + "="*60)
print("=== File-like Objects with Duck Typing ===")
print("="*60)
# Real file object
import io
real_file = io.StringIO("Initial content")
# Our custom file-like objects
string_io = StringIO("Hello StringIO")
network_stream = NetworkStream("example.com", 80)
network_stream.connect()
# Process all file-like objects the same way
file_like_objects = [real_file, string_io, network_stream]
for file_obj in file_like_objects:
process_file_like_object(file_obj, "Additional data")
# =============================================
# EXAMPLE 3: Payment Processors with Duck Typing
# =============================================
class CreditCardProcessor:
"""Credit card payment processor"""
def __init__(self, card_number, expiry_date, cvv):
self.card_number = card_number
self.expiry_date = expiry_date
self.cvv = cvv
def process_payment(self, amount):
"""Process credit card payment"""
# Simulate credit card processing
print(f" Processing ${amount} via Credit Card")
print(f" Card: **** **** **** {self.card_number[-4:]}")
print(f" Expiry: {self.expiry_date}")
return {
'success': True,
'transaction_id': f"CC_{abs(hash(self.card_number))}",
'amount': amount,
'method': 'Credit Card'
}
def refund(self, transaction_id, amount):
"""Process refund"""
print(f" Refunding ${amount} for transaction {transaction_id}")
return True
class PayPalProcessor:
"""PayPal payment processor"""
def __init__(self, email):
self.email = email
self.logged_in = False
def login(self, password):
"""Login to PayPal"""
self.logged_in = True
return True
def process_payment(self, amount):
"""Process PayPal payment"""
if not self.logged_in:
raise Exception("Not logged into PayPal")
print(f" Processing ${amount} via PayPal")
print(f" Account: {self.email}")
return {
'success': True,
'transaction_id': f"PP_{abs(hash(self.email))}",
'amount': amount,
'method': 'PayPal'
}
def get_balance(self):
"""Get PayPal balance"""
return 1000.00 # Simulated balance
class BankTransferProcessor:
"""Bank transfer payment processor"""
def __init__(self, account_number, routing_number):
self.account_number = account_number
self.routing_number = routing_number
def process_payment(self, amount):
"""Process bank transfer"""
print(f" Processing ${amount} via Bank Transfer")
print(f" Account: ****{self.account_number[-4:]}")
print(f" Routing: {self.routing_number}")
return {
'success': True,
'transaction_id': f"BT_{abs(hash(self.account_number))}",
'amount': amount,
'method': 'Bank Transfer'
}
def get_account_details(self):
"""Get account details"""
return {
'account_number': self.account_number,
'routing_number': self.routing_number
}
class ShoppingCart:
"""Shopping cart that accepts any payment processor"""
def __init__(self):
self.items = []
self.total = 0
def add_item(self, item, price):
"""Add item to cart"""
self.items.append((item, price))
self.total += price
print(f"Added {item} for ${price}")
def checkout(self, payment_processor):
"""
Checkout using any payment processor
Duck typing: only needs process_payment() method
"""
print(f"\nChecking out ${self.total:.2f}")
# Don't check type, just try to use it
try:
# This is duck typing in action!
result = payment_processor.process_payment(self.total)
if result.get('success'):
print(f"✅ Payment successful!")
print(f" Transaction ID: {result['transaction_id']}")
print(f" Method: {result['method']}")
self.items = []
self.total = 0
return True
else:
print(f"❌ Payment failed")
return False
except AttributeError:
print(f"❌ Error: Payment processor must have process_payment() method")
return False
except Exception as e:
print(f"❌ Payment error: {e}")
return False
print("\n" + "="*60)
print("=== Payment Processors with Duck Typing ===")
print("="*60)
# Create shopping cart
cart = ShoppingCart()
cart.add_item("Python Book", 49.99)
cart.add_item("Coffee Mug", 19.99)
cart.add_item("T-shirt", 24.99)
# Different payment processors
credit_card = CreditCardProcessor("4111111111111111", "12/25", "123")
paypal = PayPalProcessor("user@example.com")
paypal.login("password123")
bank_transfer = BankTransferProcessor("123456789", "021000021")
# Try all payment processors (duck typing!)
payment_methods = [
("Credit Card", credit_card),
("PayPal", paypal),
("Bank Transfer", bank_transfer)
]
for method_name, processor in payment_methods:
print(f"\n--- Trying {method_name} ---")
cart.checkout(processor)
# =============================================
# EXAMPLE 4: Iterator Protocol with Duck Typing
# =============================================
class Countdown:
"""Custom countdown iterator"""
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
class Fibonacci:
"""Fibonacci sequence generator"""
def __init__(self, limit):
self.limit = limit
self.a, self.b = 0, 1
self.count = 0
def __iter__(self):
return self
def __next__(self):
if self.count >= self.limit:
raise StopIteration
if self.count == 0:
result = self.a
elif self.count == 1:
result = self.b
else:
result = self.a + self.b
self.a, self.b = self.b, result
self.count += 1
return result
class FileLines:
"""File line iterator"""
def __init__(self, filename):
self.filename = filename
self.file = open(filename, 'r')
def __iter__(self):
return self
def __next__(self):
line = self.file.readline()
if not line:
self.file.close()
raise StopIteration
return line.strip()
def __del__(self):
if hasattr(self, 'file') and not self.file.closed:
self.file.close()
def process_iterable(iterable, description):
"""Process any iterable using duck typing"""
print(f"\n{description}:")
# Duck typing: we don't care about the type,
# only that it's iterable (has __iter__ and __next__)
try:
for item in iterable:
print(f" {item}", end=" ")
print()
except TypeError:
print(f" Error: {description} is not iterable")
print("\n" + "="*60)
print("=== Iterator Protocol with Duck Typing ===")
print("="*60)
# Create various iterables
countdown = Countdown(5)
fibonacci = Fibonacci(10)
range_obj = range(1, 6)
list_obj = ["apple", "banana", "cherry"]
dict_obj = {"a": 1, "b": 2, "c": 3}
# Process all iterables the same way
iterables = [
(countdown, "Countdown from 5"),
(fibonacci, "First 10 Fibonacci numbers"),
(range_obj, "Range 1-5"),
(list_obj, "List of fruits"),
(dict_obj, "Dictionary items")
]
for iterable, description in iterables:
process_iterable(iterable, description)
# =============================================
# DUCK TYPING BENEFITS AND CAVEATS
# =============================================
print("\n" + "="*60)
print("=== Duck Typing: Benefits and Caveats ===")
print("="*60)
print("\n✅ Benefits of Duck Typing:")
print("1. Flexibility: Code works with any object that has required methods")
print("2. Loose Coupling: No dependency on specific classes/interfaces")
print("3. Pythonic: Follows Python's dynamic nature")
print("4. Testability: Easy to create mock objects for testing")
print("5. Extensibility: New types can be added without modifying existing code")
print("\n⚠️ Caveats of Duck Typing:")
print("1. Runtime Errors: Type errors occur at runtime, not compile time")
print("2. Documentation: Must document expected interface clearly")
print("3. Debugging: Can be harder to debug interface mismatches")
print("4. Intent: Less explicit about expected types")
print("\n🎯 When to Use Duck Typing:")
print("• When working with protocols (file-like, iterator-like)")
print("• When you want maximum flexibility")
print("• When creating library/framework code")
print("• When the interface is simple and well-defined")
print("\n🎯 When to Use Explicit Typing:")
print("• For public APIs where interface should be clear")
print("• When type safety is critical")
print("• When working with complex interfaces")
print("• When using type hints for better IDE support")
Real-world Polymorphism Applications
Game Development
Game entities with polymorphic behavior:
class GameObject:
def update(self): pass
def render(self): pass
class Player(GameObject):
def update(self):
# Player-specific update
pass
def render(self):
# Render player sprite
pass
Graphics System
Drawing shapes polymorphically:
class Shape:
def draw(self): pass
class Circle(Shape):
def draw(self):
# Draw circle algorithm
return "Drawing circle"
Database ORM
Database operations with polymorphism:
class Database:
def save(self): pass
class MySQL(Database):
def save(self):
# MySQL specific save
return "Saved to MySQL"
Export System
Export data in different formats:
class Exporter:
def export(self, data): pass
class CSVExporter(Exporter):
def export(self, data):
# Export to CSV
return "Exported to CSV"
Polymorphism Best Practices
- Use meaningful method names: Overridden methods should maintain semantic consistency
- Follow Liskov Substitution Principle: Subclasses should be substitutable for base classes
- Document interfaces: Clearly document what methods subclasses should implement
- Use ABC for abstract methods:
from abc import ABC, abstractmethodfor clarity - Be consistent with operator overloading: Follow mathematical conventions
- Handle edge cases: Consider what happens with invalid operations
- Use duck typing judiciously: Balance flexibility with clarity
- Test polymorphic behavior: Ensure all implementations work correctly
- Avoid deep inheritance hierarchies: Favor composition over deep inheritance
- Use type hints for clarity:
def process(obj: Shape) -> None:
from abc import ABC, abstractmethod
from typing import List, Union
# =============================================
# GOOD PRACTICE: Using ABC for clear interfaces
# =============================================
class Renderable(ABC):
"""Abstract base class for renderable objects"""
@abstractmethod
def render(self):
"""Render the object to screen"""
pass
@abstractmethod
def get_bounding_box(self):
"""Get object's bounding box"""
pass
class Circle(Renderable):
def __init__(self, x, y, radius):
self.x = x
self.y = y
self.radius = radius
def render(self):
"""Render circle"""
return f"Rendering circle at ({self.x}, {self.y}) with radius {self.radius}"
def get_bounding_box(self):
"""Get circle bounding box"""
return (self.x - self.radius, self.y - self.radius,
self.x + self.radius, self.y + self.radius)
# =============================================
# GOOD PRACTICE: Type hints for polymorphic functions
# =============================================
def process_shapes(shapes: List[Renderable]) -> None:
"""Process multiple shapes polymorphically"""
for shape in shapes:
print(shape.render())
print(f" Bounding box: {shape.get_bounding_box()}")
# =============================================
# GOOD PRACTICE: Consistent operator overloading
# =============================================
class Money:
"""Money class with proper operator overloading"""
def __init__(self, amount: float, currency: str = "USD"):
if amount < 0:
raise ValueError("Amount cannot be negative")
self.amount = amount
self.currency = currency
def __add__(self, other: 'Money') -> 'Money':
if not isinstance(other, Money):
raise TypeError("Can only add Money to Money")
if self.currency != other.currency:
raise ValueError("Cannot add different currencies")
return Money(self.amount + other.amount, self.currency)
def __sub__(self, other: 'Money') -> 'Money':
if not isinstance(other, Money):
raise TypeError("Can only subtract Money from Money")
if self.currency != other.currency:
raise ValueError("Cannot subtract different currencies")
return Money(self.amount - other.amount, self.currency)
def __mul__(self, factor: Union[int, float]) -> 'Money':
if not isinstance(factor, (int, float)):
raise TypeError("Can only multiply by scalar")
return Money(self.amount * factor, self.currency)
def __rmul__(self, factor: Union[int, float]) -> 'Money':
return self.__mul__(factor)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Money):
return False
return self.amount == other.amount and self.currency == other.currency
def __str__(self) -> str:
return f"{self.currency} {self.amount:.2f}"
def __repr__(self) -> str:
return f"Money({self.amount}, '{self.currency}')"
# =============================================
# GOOD PRACTICE: Duck typing with defensive programming
# =============================================
class DataProcessor:
"""Process data from various sources"""
@staticmethod
def process(data_source):
"""
Process data from any source that supports read()
Args:
data_source: Any object with a read() method
"""
# Defensive check
if not hasattr(data_source, 'read'):
raise TypeError("data_source must have a read() method")
try:
data = data_source.read()
# Process data...
return f"Processed {len(data)} bytes"
except Exception as e:
return f"Error processing data: {e}"
# =============================================
# COMPARISON: Good vs Bad Polymorphism
# =============================================
print("=== Good Polymorphism Practices ===")
# Good: Clear interface with ABC
circle = Circle(10, 20, 5)
print(f"Circle: {circle.render()}")
# Good: Proper operator overloading
m1 = Money(100)
m2 = Money(50)
print(f"\nMoney operations:")
print(f"m1 + m2 = {m1 + m2}")
print(f"m1 - m2 = {m1 - m2}")
print(f"m1 * 2 = {m1 * 2}")
# Good: Defensive duck typing
class StringReader:
def __init__(self, data):
self.data = data
def read(self):
return self.data
reader = StringReader("Hello World")
result = DataProcessor.process(reader)
print(f"\nData processing: {result}")
print("\n" + "="*60)
print("=== Polymorphism Anti-patterns to Avoid ===")
print("="*60)
print("\n❌ Anti-pattern 1: Breaking Liskov Substitution")
print(" - Subclass that changes method semantics")
print(" - Overriding method with completely different behavior")
print("\n❌ Anti-pattern 2: Operator overloading with unexpected behavior")
print(" - Overloading + to do subtraction")
print(" - Inconsistent mathematical operations")
print("\n❌ Anti-pattern 3: Fragile duck typing")
print(" - Assuming too much about objects")
print(" - Not handling missing methods gracefully")
print("\n❌ Anti-pattern 4: Deep inheritance hierarchies")
print(" - Class inheriting from grandparent's grandparent")
print(" - Method resolution becomes confusing")
print("\n✅ Follow these principles:")
print("1. Single Responsibility: Each class has one reason to change")
print("2. Open/Closed: Open for extension, closed for modification")
print("3. Liskov Substitution: Subclasses are substitutable for base")
print("4. Interface Segregation: Many specific interfaces")
print("5. Dependency Inversion: Depend on abstractions, not concretions")
Python Polymorphism Key Takeaways
- Polymorphism means "many forms" - same interface, different implementations
- Method Overriding: Child classes provide specific implementations of parent methods
- Operator Overloading: Customize behavior of operators (+, -, *, etc.) using dunder methods
- Duck Typing: Focus on object behavior rather than type ("if it quacks like a duck...")
- Use
super()to call parent class methods from overridden methods - Special methods like
__add__,__str__,__len__enable operator overloading - Duck typing enables flexible, loosely-coupled code but requires careful documentation
- Use Abstract Base Classes (ABC) for clear interface definitions
- Follow Liskov Substitution Principle: subclasses should be substitutable for parent classes
- Polymorphism makes code more extensible, maintainable, and reusable