Skip to content

In-Depth Guide to Polymorphism in Python Using Different Classes

Updated: at 05:45 AM

Polymorphism is a core concept in object-oriented programming that allows objects of different classes to be used interchangeably if they share a common interface. It provides a way to create flexible and reusable code that can work with objects of various types.

In Python, polymorphism is supported through abstract base classes and interfaces. Python programmers leverage polymorphism to write general functions and methods that can handle objects from various subclasses or implementations. This allows for more dynamic and extendable code.

This comprehensive guide will explain polymorphism in detail with a focus on using different classes through a common interface in Python. We will cover key topics including:

Table of Contents

Open Table of Contents

What is Polymorphism?

Polymorphism stems from the Greek words poly meaning many and morph meaning form. It refers to the ability of different object classes to implement the same interface or method signature.

In simple terms, polymorphism allows objects with different underlying forms to be treated as if they are of the same type. A single interface or method call can be applied to objects from multiple class types seamlessly.

For example, think of the len() function in Python. It can operate on a string, list, tuple, dictionary or other objects that implement the special __len__() method:

print(len("Hello")) # 5
print(len([1, 2, 3])) # 3
print(len((1, 2))) # 2

The len() function exhibits polymorphism as it works on different data type objects due to the presence of the common __len__() method.

Some key advantages of polymorphism are:

Enabling Polymorphism in Python

There are two main ways to enable polymorphic behavior in Python:

  1. Abstract base classes - These define a generic interface, including abstract methods, that concrete subclasses inherit and implement.

  2. Duck typing - Objects of different unrelated types are used polymorphically when they have compatible interfaces. Duck typing is a more informal type of polymorphism.

We will focus on abstract base classes, which provide a more strict and formal polymorphism mechanism.

Abstract Base Classes

Abstract base classes (ABCs) define a template with unimplemented abstract methods and properties that subclasses must override. This creates a common interface enforced on all subclasses.

Python provides the abc module to help create abstract base classes.

For example, we can define a base class Animal with an abstract make_sound() method:

from abc import ABC, abstractmethod

class Animal(ABC):

    @abstractmethod
    def make_sound(self):
        pass

This Animal ABC defines the interface for all subclasses. Any concrete Animal subclasses we now create must implement make_sound(), otherwise they will raise a TypeError.

Let’s define some subclasses:

class Dog(Animal):

    def make_sound(self):
        print("Bark!")

class Cat(Animal):

    def make_sound(self):
        print("Meow")

Now we can write polymorphic functions that use the common Animal interface:

def animal_sound(animals: list[Animal]):
    for animal in animals:
        animal.make_sound()

dog = Dog()
cat = Cat()

animal_sound([dog, cat])

This will output:

Bark!
Meow

Even though Dog and Cat are different classes, we can iterate through them polymorphically using the common make_sound() method from the Animal interface.

ABC Syntax

The key aspects of writing abstract base classes in Python are:

This syntax allows ABCs to clearly define the required interface for subclasses.

ABC Example - Graphical Shape Classes

Let’s see a more complete example of using ABCs to implement polymorphic classes.

We’ll define an abstract Shape class with area() and perimeter() methods:

from abc import ABC, abstractmethod, abstractproperty

class Shape(ABC):

    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

Then we can create concrete shape classes like Square and Circle:

from math import pi

class Square(Shape):

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

    def area(self):
        return self.side ** 2

    def perimeter(self):
        return 4 * self.side

class Circle(Shape):

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

    def area(self):
        return pi * self.radius ** 2

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

And use them polymorphically:

square = Square(5)
circle = Circle(3)

shapes = [square, circle]

for shape in shapes:
    print(shape.area())
    print(shape.perimeter())

This will output the different areas and perimeters for the square and circle, allowing polymorphic use through their shared base Shape interface.

The Shape ABC enforces that all subclasses like Square and Circle must implement the area() and perimeter() methods. This guarantees the common interface required for polymorphism.

The collections.abc Module

Python comes with several built-in abstract base classes for common data structures in the collections.abc module:

For example, we can write a function to sum any Sequence:

from collections.abc import Sequence

def sum_sequence(seq: Sequence) -> int:

    total = 0
    for x in seq:
        total += x
    return total

And apply it to different sequence types:

sum_sequence([1, 2, 3]) # 6
sum_sequence((4, 5, 6)) # 15
sum_sequence(range(7, 10)) # 24

The built-in collections ABCs like Sequence provide convenient common interfaces for core Python types.

Interfaces in Python

While ABCs provide one way to create polymorphic class hierarchies, interfaces provide an alternative method without needing abstract classes.

Interfaces define the method signatures that classes must implement without any implementation. This separates the interface from the class hierarchy.

For example, we can define a simple Sounder interface:

from abc import abstractmethod

class Sounder:

    @abstractmethod
    def make_sound(self):
        pass

Then unrelated classes just need to implement this interface to enable polymorphic usage:

class Dog:

    def make_sound(self):
        print("Bark!")

class Car:

    def make_sound(self):
        print("Honk!")

def make_noise(sounder):
    sounder.make_sound()

Here Dog and Car don’t inherit from a common base but implement the same Sounder interface to be used polymorphically.

Interfaces are useful when you want to separate unrelated classes that just need to share common method signatures.

Method Overriding

A key aspect of polymorphism in OOP is method overriding. This means subclasses can override the implementation of methods inherited from parent classes.

For example:

class Animal:

    def make_sound(self):
        print("Some noise")

class Dog(Animal):

    def make_sound(self):
        print("Bark!")

Here Dog overrides the general make_sound() method to provide specialized behavior.

The version in Dog will be called polymorphically even though make_sound() originally comes from Animal:

dog = Dog()
dog.make_sound() # "Bark!"

Method overriding allows subclasses to customize parent class methods to fit their needs. This helps enable polymorphic code using those methods.

Duck Typing

Duck typing is a concept related to polymorphism in dynamic languages like Python. It can be summed up by:

If it walks like a duck and talks like a duck, it’s a duck!

This means Python objects can be used polymorphically when they have the same interface or behaviors, even if they are of unrelated types. Like a duck!

For example, both str and bytes have an encode() method. So they can be used interchangeably in that context:

def encode(obj):
    print(obj.encode())

s = "Hello"
b = b"World"

encode(s)
encode(b)  # No TypeError!

Duck typing does not use formal abstract classes or interfaces. It instead relies on implicit “duck-like” interfaces.

This provides loose polymorphic behavior between otherwise unrelated classes. But code may break if a class drops or changes its interface.

When to Use Polymorphism

Here are some common use cases where polymorphism is very handy in Python:

Overall, polymorphism helps create more reusable, maintainable and adaptable systems. But it should be used judiciously when there are clear benefits.

Polymorphism vs Overloading vs Overriding

It helps to differentiate polymorphism from two related concepts - overloading and overriding.

Overloading means having methods of the same name but different signatures in the same class. For example:

class Math:

    def add(self, x, y):
        # Handle x + y

    def add(self, x, y, z):
        # Handle x + y + z

Overriding means subclasses reimplementing methods inherited from parent classes. For example:

class Animal:

    def make_sound(self):
        print("Some noise")

class Dog(Animal):

    def make_sound(self):
        print("Bark!")

Polymorphism is about allowing different object types to be processed through a common interface. It relies on overriding to customize implementations across subclasses.

Practical Example - Game Characters

Let’s see a practical example of using ABCs and polymorphism to implement game character classes.

First, we define a Character base class with shared attributes like name and level:

from abc import ABC, abstractmethod

class Character(ABC):

    def __init__(self, name, level):
        self.name = name
        self.level = level

Next we create a PlayableCharacter ABC that adds attack() and heal() methods:

class PlayableCharacter(Character, ABC):

    @property
    @abstractmethod
    def stats(self): pass

    @abstractmethod
    def attack(self): pass

    @abstractmethod
    def heal(self): pass

Now we can extend this with specific character types:

from random import randint

class Warrior(PlayableCharacter):

    def __init__(self, name, level):
        super().__init__(name, level)
        self.strength = randint(1, 10)

    @property
    def stats(self):
        return {"strength": self.strength}

    def attack(self):
        return self.strength * 2

    def heal(self):
        pass

class Mage(PlayableCharacter):

    def __init__(self, name, level):
        super().__init__(name, level)
        self.magic = randint(1, 10)

    @property
    def stats(self):
        return {"magic": self.magic}

    def attack(self):
        return self.magic

    def heal(self):
        return self.magic * 1.5

Let’s create some characters:

warrior = Warrior("Throgdar", 5)
mage = Mage("Elminster", 8)

And now we can process them generically through the base Character interface:

characters = [warrior, mage]

for character in characters:
    print(character.name)
    print(character.level)

But also use the subclasses polymorphically:

for character in characters:
    print(character.name, "attack:", character.attack())
    print(character.name, "heal:", character.heal())

This allows both shared processing through the base as well as subclass-specific polymorphic behavior.

Conclusion

Polymorphism is a powerful concept in object-oriented programming and Python makes it easy to implement polymorphic behavior using abstract base classes or duck typing.

Key points:

I hope this comprehensive guide gives you a deep understanding of implementing polymorphism in Python using abstract classes and other techniques! Let me know if you have any other questions.