Inheritance and Polymorphism

Author

Davide Vitiello, Mirai Solutions GmbH

Published

March 11, 2025

Inheritance and polymorphism are foundational concepts of object-oriented programming (OOP) that Python supports with its versatile syntax.
They allow for the creation of a hierarchical classification of classes, enabling more complex and nuanced object relationships and behaviors.

Inheritance

Inheritance allows a class (known as a child or subclass) to inherit attributes and methods from another class (known as a parent or superclass). This mechanism promotes code reuse and a hierarchical organization of classes.

Basic Inheritance

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        raise NotImplementedError("Subclass must implement abstract method")
    
    def begets_offspring(self) -> bool:
        return True

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

class Liger(Animal):
    def speak(self):
        return f"{self.name} says Roar!"

    def begets_offspring(self):
        return False

class Pet(Animal):
    def __init__(self, name, owner):
        super().__init__(name)
        self.owner = owner

    def speak(self):
        return f"{self.name} probably says Woof or Meow"

animals = [Dog("Scooby"), Cat("Snowball"), Liger("Half Tiger Half Lion")]
print([f"{animal.name} {animal.speak()}" for animal in animals])
print([f"{animal.name} is sterile" for animal in animals if not animal.begets_offspring()])
['Scooby Scooby says Woof!', 'Snowball Snowball says Meow!', 'Half Tiger Half Lion Half Tiger Half Lion says Roar!']
['Half Tiger Half Lion is sterile']

We don’t need to define again .__init__() in the subclasses when no attributes are added to those already defined in the superclass. By leaving out the constructor definition, the subclass inherits the constructor definition from the superclass. To include new attributes (as owner in Pet), we need to redefine .__init__() and explicitly call the superclass constructor using super().__init__.

The definition of Animal.speak() in the example forces the subclasses to define an overriding method, otherwise an error will be raised.

misterious_animal = Animal("X")
misterious_animal.speak()
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
Cell In[2], line 2
      1 misterious_animal = Animal("X")
----> 2 misterious_animal.speak()

Cell In[1], line 6, in Animal.speak(self)
      5 def speak(self):
----> 6     raise NotImplementedError("Subclass must implement abstract method")

NotImplementedError: Subclass must implement abstract method

On the other hand, .begets_offspring(), is rather an “optional” method for subclasses of Animal and will take the default value defined in the superclass if lacking.

When using super() and adding new attributes in the subclass, we can also set them a default value in the constructor definition as show in the example for the attribute .can_fly. This way we can instantiate the subclass the same way we would with the superclass, in our case, only by assignign name to an Animal:

class Bird(Animal):
    def __init__(self, name, can_fly=True):
        super().__init__(name)
        self.can_fly = can_fly

    def speak(self):
        return f"{self.name} says Tweet!"

    def fly(self):
        return "Flies away" if self.can_fly else f"{self.name} can't fly."

parrot = Bird("Polly")
print(parrot.speak())
print(parrot.fly())
Polly says Tweet!
Flies away
ostrich = Bird("Ollie", can_fly=False)
print(ostrich.fly())
Ollie can't fly.

Multiple Inheritance

Via multiple inheritance, a class is allowed to inherit from more than one parent class.

class Swimming:
    def swim(self):
        return "Swims forward"

class TerrestrialAnimal(Animal):
    def walk(self):
        return "walks on land"

class AquaticAnimal(Animal, Swimming):
    def swim(self):
        return "swims in water"

class Fish(AquaticAnimal):
    def speak(self):
        return "says ... (*mute*)"

class Horse(TerrestrialAnimal):
    def speak(self):
        return "*neighs in high pitch*"

    def walk(self):
        return "trots on land"

    def rears_up(self):
        return "rears up on 2 legs"
goldfish = AquaticAnimal("Nemo")
print(f"{goldfish.name} {goldfish.swim()}")
Nemo swims in water
goldfish = Fish("Dory")
print(f"{goldfish.name} {goldfish.speak()} and {goldfish.swim()}")
Dory says ... (*mute*) and swims in water
bthunder_horse = Horse("Black Thunder")
print(f"{bthunder_horse.name} {bthunder_horse.speak()} and {bthunder_horse.rears_up()}")
Black Thunder *neighs in high pitch* and rears up on 2 legs

Notice how TerrestrialAnimal and AcquaticAnimal do not implement a .speak() method, hence it can’t be used to instantiate an object directly. This way, we can enforce a certain intermediate inheritance to exist without giving it “permission” to make objects directly.

Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common superclass. In practice, it also allows methods to be defined in a way that they can operate on object of difference classes. This shows usefulness when they share a method name but implement it differently. The net result of this is, two classes can be in certain contextes be used interchageably.

Polymorphism can be achieved through inheritance, albeit it doesn’t necessarily need it.

Polymorphism with Inheritance

animals = [Dog("Otto"), Cat("Stella"), Fish("Nemo"), Horse("BlackThunder")]

for animal in animals:
    print(f"{animal.name}: {animal.speak()}")
    if isinstance(animal, Swimming):
        print(f"{animal.name} can also swim!")
Otto: Otto says Woof!
Stella: Stella says Meow!
Nemo: says ... (*mute*)
Nemo can also swim!
BlackThunder: *neighs in high pitch*

Each of the classes in the animals list has its own implementation of the speak method, which allows them to produce different outputs when the method is called. All the classes of the object at hand implement speak differently and yet we can call the different implementation seamlessly using the single method name speak.

Polymorphism without Inheritance

Python also supports duck typing, which allows for polymorphism without a formal inheritance hierarchy.

class Robot:
    def speak(self):
        return "Beep boop"

def animal_sound(animal):
    print(animal.speak())

# Duck typing 
animal_sound(Dog("RoboDog"))
animal_sound(Robot())
RoboDog says Woof!
Beep boop

animal_sound() is called with instances of both Dog and Robot, showing that both can be used interchangeably because they both implement the speak method, without requiring a formal hierarchy.

Duck typing is a concept that emphasizes an object’s behavior over its actual type. The name is derived from the saying “If it looks like a duck and quacks like a duck, it must be a duck.”. This principle allows for polymorphism without the need for a formal inheritance structure.
In duck typing, the focus is on what an object can do, rather than what an object is. This means that if an object implements a certain method or behavior, it can be used in any context that expects that method or behavior, regardless of the object’s class.

Back to top