Object-Oriented Programming

Python Polymorphism

Learn about polymorphism in Python - duck typing, method overriding, operator overloading, and abstract classes.

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.

python
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:

python
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:

python
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)          # True

Polymorphism with Built-in Functions

Python built-ins use polymorphism extensively:

python
# 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 A

Abstract Classes

Use the abc module to define abstract classes that enforce method implementation:

python
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:

python
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 square

Summary

  • 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.