Post

Understanding Polymorphism in Python: A Guide to Flexible and Dynamic Code

Understanding Polymorphism in Python: A Guide to Flexible and Dynamic Code

Introduction

In the world of Object-Oriented Programming (OOP), there are a few core concepts that provide the paradigm with its power and elegance. Among them, polymorphism stands out as a particularly potent idea. The term itself comes from Greek, meaning “many forms,” which is a fitting description for a principle that allows a single interface to represent different underlying types.

Python, with its dynamic and flexible nature, embraces polymorphism in ways that are both powerful and intuitive. Understanding how to leverage it can dramatically improve your code’s design, making it more modular, reusable, and easier to maintain. This post will explore the different facets of polymorphism in Python, from classic inheritance to the uniquely Pythonic “duck typing.”

What is Polymorphism?

At its core, polymorphism is the ability of an object to take on many forms. In practice, this means you can have multiple classes with different implementations of the same method. A function can then call this method without knowing or caring about the specific class of the object it’s working with. It simply trusts that the object knows how to handle the method call.

Think of a real-world analogy: the “start” button on a vehicle. Whether you’re in a car, on a motorcycle, or piloting a boat, you understand the concept of “starting” the engine. The action is the same—you initiate a process—but the underlying mechanics (the implementation) are vastly different. Polymorphism in code works the same way; we can call vehicle.start() and trust that the car, motorcycle, or boat object will do the right thing.

1. Polymorphism through Inheritance (Method Overriding)

The most traditional way to achieve polymorphism is through inheritance. A base class defines a method, and one or more subclasses provide their own specific implementation of that method. This is called method overriding.

Let’s model a few animals. We can define a base class Animal with a speak() method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Base class
class Animal:
    def speak(self):
        raise NotImplementedError("Subclass must implement abstract method")

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

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

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

# A function that uses the polymorphic behavior
def make_animal_speak(animal: Animal):
    print(f"The animal says: {animal.speak()}")

# Create instances of the subclasses
dog = Dog()
cat = Cat()
bird = Bird()

# Call the function with different objects
make_animal_speak(dog)   # Output: The animal says: Woof!
make_animal_speak(cat)   # Output: The animal says: Meow!
make_animal_speak(bird)  # Output: The animal says: Chirp!

In this example, the make_animal_speak function doesn’t need to know if it’s dealing with a Dog, Cat, or Bird. It only needs to know that the object it receives is a type of Animal and thus has a speak() method. The correct version of speak() is called automatically based on the object’s actual class. This makes our make_animal_speak function flexible and decoupled from the specific animal implementations.

2. Duck Typing: The Pythonic Way

Python’s dynamic nature gives rise to a more informal and incredibly powerful form of polymorphism known as duck typing. The name comes from the saying:

“If it walks like a duck and it quacks like a duck, then it must be a duck.”

In programming terms, this means Python doesn’t care about an object’s type, only about its behavior. If an object has the methods and properties required for a certain operation, it can be used in that operation, regardless of its class or inheritance hierarchy.

Let’s consider a function that needs to iterate over a collection.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

class Bookshelf:
    def __init__(self):
        self._books = []

    def add_book(self, book):
        self._books.append(book)

    # By implementing __len__, this object can be used with the len() function
    def __len__(self):
        return len(self._books)

# These objects are not related by inheritance
my_list = [1, 2, 3, 4]
my_string = "Hello, World!"
my_bookshelf = Bookshelf()
my_bookshelf.add_book(Book("Fluent Python", "Luciano Ramalho"))

# The built-in len() function demonstrates duck typing
print(f"Length of list: {len(my_list)}")         # Output: Length of list: 4
print(f"Length of string: {len(my_string)}")     # Output: Length of string: 13
print(f"Length of bookshelf: {len(my_bookshelf)}") # Output: Length of bookshelf: 1

Here, list, str, and our custom Bookshelf class are completely unrelated. However, because they all implement the __len__ dunder method, the built-in len() function can work with all of them. The len() function doesn’t check if the object is a list or str; it just checks if it “quacks” like something that has a length.

3. Polymorphism with Abstract Base Classes (ABCs)

Duck typing is great for flexibility, but sometimes you need a more formal contract. You might want to guarantee that a class implements a certain set of methods. This is where Abstract Base Classes (ABCs) come in. The abc module allows you to define an interface that subclasses are required to follow.

This approach combines the structural guarantees of inheritance-based polymorphism with the flexibility of duck typing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from abc import ABC, abstractmethod

# Define an abstract base class
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

# Implement concrete subclasses
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

# A function that works with any Shape
def print_shape_details(shape: Shape):
    print(f"Area: {shape.area()}")
    print(f"Perimeter: {shape.perimeter()}")

# Using the function with different shapes
rectangle = Rectangle(10, 5)
circle = Circle(7)

print("Rectangle Details:")
print_shape_details(rectangle)

print("\nCircle Details:")
print_shape_details(circle)

# What happens if we forget to implement a method?
# The following line would raise a TypeError because Triangle doesn't implement 'perimeter'
# class Triangle(Shape):
#     def area(self):
#         return 10
# t = Triangle() # This would fail

By using @abstractmethod, we declare that any concrete subclass of Shape must provide its own implementation for area and perimeter. This ensures that any object claiming to be a Shape will have the methods our print_shape_details function expects.

Conclusion

Polymorphism is a cornerstone of good object-oriented design, and Python provides multiple ways to achieve it.

  • Method Overriding gives us the classic, structured approach based on inheritance.
  • Duck Typing offers a flexible, uniquely Pythonic way to focus on an object’s behavior rather than its type.
  • Abstract Base Classes provide a middle ground, allowing you to enforce contracts and create explicit interfaces when needed.

By understanding and applying these different forms of polymorphism, you can write code that is more abstract, less coupled, and far more adaptable to change. It encourages you to think in terms of interfaces and behaviors, leading to cleaner, more maintainable, and ultimately more powerful software.

Suggested Reading

This post is licensed under CC BY 4.0 by the author.