backend / python oop / 04_polymorphism_in_python.md

Polymorphism in Python

4 interview angles 7 min read source

Polymorphism in Python

Polymorphism is one of the four fundamental principles of Object-Oriented Programming (OOP). It allows objects of different classes to be treated as objects of a common superclass, enabling code to work with objects of multiple types through a unified interface.

The word “polymorphism” comes from Greek words meaning “many forms.”


Types of Polymorphism

1. Method Overriding (Runtime Polymorphism)

  • Subclasses provide specific implementations of methods defined in their superclass
  • The method to be called is determined at runtime based on the object’s actual type
class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

class Bird(Animal):
    def speak(self):
        return "Tweet!"

# Polymorphic behavior
def animal_sound(animal):
    return animal.speak()

# Different objects, same interface
dog = Dog()
cat = Cat()
bird = Bird()

print(animal_sound(dog))   # Woof!
print(animal_sound(cat))   # Meow!
print(animal_sound(bird))  # Tweet!

2. Method Overloading (Compile-time Polymorphism)

  • Python doesn’t support traditional method overloading like Java/C++
  • But we can achieve similar functionality using default parameters and variable arguments
class Calculator:
    def add(self, a, b, c=None):
        if c is None:
            return a + b
        else:
            return a + b + c

    def multiply(self, *args):
        result = 1
        for num in args:
            result *= num
        return result

calc = Calculator()
print(calc.add(5, 3))        # 8 (2 parameters)
print(calc.add(5, 3, 2))     # 10 (3 parameters)
print(calc.multiply(2, 3))   # 6
print(calc.multiply(2, 3, 4)) # 24

Operator Overloading

Python allows you to define how operators work with your custom objects:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Point(self.x - other.x, self.y - other.y)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __str__(self):
        return f"Point({self.x}, {self.y})"

    def __len__(self):
        return int((self.x**2 + self.y**2)**0.5)

p1 = Point(1, 2)
p2 = Point(3, 4)

print(p1 + p2)    # Point(4, 6)
print(p2 - p1)    # Point(2, 2)
print(p1 == p2)   # False
print(len(p1))    # 2 (distance from origin)

Duck Typing

Python’s dynamic typing allows for “duck typing” - if it walks like a duck and quacks like a duck, it’s a duck:

class Duck:
    def swim(self):
        return "Duck swimming"

    def quack(self):
        return "Quack quack!"

class RubberDuck:
    def swim(self):
        return "Rubber duck floating"

    def quack(self):
        return "Squeak squeak!"

class Person:
    def swim(self):
        return "Person swimming"

    def quack(self):
        return "Person quacking!"

# Polymorphic function - works with any object that has swim and quack methods
def make_it_swim_and_quack(duck_like_object):
    print(duck_like_object.swim())
    print(duck_like_object.quack())

# All these work, even though they're different types
make_it_swim_and_quack(Duck())
make_it_swim_and_quack(RubberDuck())
make_it_swim_and_quack(Person())

Abstract Base Classes (ABCs)

ABCs provide a way to define interfaces and enforce polymorphism:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    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):
        import math
        return math.pi * self.radius ** 2

    def perimeter(self):
        import math
        return 2 * math.pi * self.radius

# Polymorphic function
def print_shape_info(shape):
    print(f"Area: {shape.area():.2f}")
    print(f"Perimeter: {shape.perimeter():.2f}")

rect = Rectangle(5, 3)
circle = Circle(4)

print_shape_info(rect)   # Works with Rectangle
print_shape_info(circle) # Works with Circle

Function Polymorphism

Functions can be polymorphic by accepting different types:

def process_data(data):
    """Polymorphic function that works with different data types"""
    if isinstance(data, str):
        return data.upper()
    elif isinstance(data, list):
        return [item.upper() if isinstance(item, str) else item for item in data]
    elif isinstance(data, dict):
        return {k: v.upper() if isinstance(v, str) else v for k, v in data.items()}
    else:
        return str(data)

# Same function, different types
print(process_data("hello"))                    # HELLO
print(process_data(["hello", "world"]))         # ['HELLO', 'WORLD']
print(process_data({"greeting": "hello"}))      # {'greeting': 'HELLO'}
print(process_data(42))                         # 42

Built-in Polymorphism

Python’s built-in functions are polymorphic:

# len() works with different types
print(len("hello"))      # 5 (string)
print(len([1, 2, 3]))    # 3 (list)
print(len({"a": 1}))     # 1 (dict)

# + operator works with different types
print("hello" + " world")  # hello world (string concatenation)
print([1, 2] + [3, 4])     # [1, 2, 3, 4] (list concatenation)
print(5 + 3)               # 8 (addition)

# print() works with any type
print("String")
print(42)
print([1, 2, 3])
print({"key": "value"})

Method Polymorphism with Inheritance

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def get_salary(self):
        return self.salary

    def get_bonus(self):
        return self.salary * 0.1

class Manager(Employee):
    def __init__(self, name, salary, department):
        super().__init__(name, salary)
        self.department = department

    def get_bonus(self):
        return self.salary * 0.2  # Managers get higher bonus

class Developer(Employee):
    def __init__(self, name, salary, programming_language):
        super().__init__(name, salary)
        self.programming_language = programming_language

    def get_bonus(self):
        return self.salary * 0.15  # Developers get medium bonus

# Polymorphic function
def calculate_total_compensation(employee):
    return employee.get_salary() + employee.get_bonus()

# Different employee types
manager = Manager("Alice", 80000, "Engineering")
developer = Developer("Bob", 70000, "Python")

print(f"{manager.name}: ${calculate_total_compensation(manager)}")
print(f"{developer.name}: ${calculate_total_compensation(developer)}")

Interface Polymorphism

Using protocols (structural typing) in Python:

from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> str:
        ...

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def draw(self):
        return f"Drawing circle with radius {self.radius}"

class Square:
    def __init__(self, side):
        self.side = side

    def draw(self):
        return f"Drawing square with side {self.side}"

class Triangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def draw(self):
        return f"Drawing triangle with base {self.base} and height {self.height}"

# Polymorphic function using protocol
def draw_shape(shape: Drawable):
    print(shape.draw())

# All these work because they implement the draw method
draw_shape(Circle(5))
draw_shape(Square(4))
draw_shape(Triangle(3, 6))

Polymorphism with Magic Methods

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def __str__(self):
        return f"Vector({self.x}, {self.y})"

    def __len__(self):
        return int((self.x**2 + self.y**2)**0.5)

class Matrix:
    def __init__(self, data):
        self.data = data

    def __add__(self, other):
        # Matrix addition logic
        return Matrix([[self.data[i][j] + other.data[i][j]
                       for j in range(len(self.data[0]))]
                       for i in range(len(self.data))])

    def __mul__(self, scalar):
        return Matrix([[self.data[i][j] * scalar
                       for j in range(len(self.data[0]))]
                       for i in range(len(self.data))])

    def __str__(self):
        return str(self.data)

# Polymorphic operations
def multiply_by_two(obj):
    return obj * 2

v = Vector(3, 4)
m = Matrix([[1, 2], [3, 4]])

print(multiply_by_two(v))  # Vector(6, 8)
print(multiply_by_two(m))  # [[2, 4], [6, 8]]

Summary Table

Type of Polymorphism Description Example
Method Overriding Subclasses override superclass methods Dog.speak() vs Cat.speak()
Method Overloading Same method with different parameters add(a, b) vs add(a, b, c)
Operator Overloading Custom behavior for operators __add__, __sub__, __eq__
Duck Typing Interface-based polymorphism Any object with required methods
Abstract Base Classes Enforced interface contracts @abstractmethod
Built-in Polymorphism Python’s built-in polymorphic functions len(), print(), +
Function Polymorphism Functions that work with multiple types process_data()

Key Interview Points

  1. Polymorphism allows treating different objects through a common interface
  2. Method overriding is runtime polymorphism (most common in Python)
  3. Duck typing is Python’s approach to polymorphism
  4. Operator overloading uses magic methods like __add__, __sub__
  5. Abstract Base Classes enforce interface contracts
  6. Built-in functions like len(), print() are polymorphic
  7. Protocols provide structural typing for polymorphism
  8. Polymorphism promotes code reusability and flexibility
  9. Python’s dynamic typing makes polymorphism natural and powerful
  10. Polymorphism is essential for writing flexible, maintainable code

Benefits of Polymorphism

  • Code Reusability: Write once, use with many types
  • Flexibility: Easy to extend with new types
  • Maintainability: Changes in one place affect all implementations
  • Readability: Code is more intuitive and expressive
  • Scalability: Easy to add new functionality without changing existing code

Polymorphism is a powerful concept that makes Python code more flexible, maintainable, and elegant!

Interview angle

  • “How does Python do polymorphism without interfaces?” — duck typing. Behaviour is determined by what an object supports at runtime, not by its declared type. Protocols make that statically checkable without requiring inheritance.
  • “Does Python support method overloading?” — not by signature; a later definition simply replaces an earlier one. Achieve the same with default arguments, *args/**kwargs, or functools.singledispatch for genuine type-based dispatch. typing.overload declares signatures for the type checker only and has no runtime effect.
  • “What does operator overloading actually do?”a + b calls a.__add__(b), falling back to b.__radd__(a) if that returns NotImplemented. Returning NotImplemented rather than raising is what lets the other operand get a chance, and it’s the part people get wrong.
  • “EAFP or LBYL?” — Python prefers EAFP: attempt the operation and handle the exception, rather than checking types up front. It’s faster in the common case and avoids a check-then-use race. hasattr chains are usually a sign you wanted a Protocol.