Skip to content

Implementing Inheritance Hierarchies and Polymorphic Behavior in Python: A Practical Guide

Updated: at 04:45 AM

Inheritance and polymorphism are two fundamental concepts in object-oriented programming (OOP) that allow code reuse and flexibility in Python. Inheritance enables a new class to acquire attributes and behaviors from an existing class, called the parent or base class. This helps reduce code duplication by factoring out common capabilities into superclass. Polymorphism allows objects with different types to respond to the same method invocation dynamically at runtime based on their class.

In this comprehensive guide, we will explore practical techniques for leveraging inheritance hierarchies and polymorphism in Python through examples and hands-on exercises. Readers will learn how to:

Table of Contents

Open Table of Contents

Implementing Single Inheritance

Single inheritance allows a Python class to inherit attributes and behaviors from one parent class. The syntax is simple - include the parent class name in parentheses when defining the new child class:

class ParentClass:
    # parent class definition

class ChildClass(ParentClass):
    # child class inherits from ParentClass

For example, we can create an Animal parent class with shared attributes like name and age. The child Cat class inherits the __init__() constructor allowing it to initialize name and age:

class Animal:
    """Parent class with shared attributes"""

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

class Cat(Animal):
    """Child class inherits Animal's __init__()"""

    def __init__(self, name, age):
        super().__init__(name, age) # call parent __init__()

felix = Cat("Felix", 3)
print(felix.name) # "Felix"
print(felix.age) # 3

The super() function gives access to the parent class allowing the Cat constructor to leverage reuse code in Animal. This demonstrates the code reuse benefits of single inheritance.

Let’s practice creating a single inheritance hierarchy:

Exercise: Implement a Vehicle parent class with num_wheels and color attributes. Create a child Bike class that inherits from it. Initialize Bike instances with a different default num_wheels value. Verify inheritance is working properly by printing the attributes.

Extending Parent Classes with Overrides

A key benefit of inheritance is that child classes can override parent class methods and attributes to specialize behavior.

To override a parent’s method, redefine the method with the same name in the child class:

class Parent:
    def method(self):
        print("Parent method")

class Child(Parent):
    def method(self):
        print("Child method") # overrides parent

child = Child()
child.method() # "Child method"

Attributes can be similarly overridden by assigning values in the child class __init__() method:

class Parent:
    def __init__(self):
        self.attr = 1

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.attr = 2 # overrides parent attr

child = Child()
print(child.attr) # 2

Let’s override a parent class method and attribute:

Exercise:

  1. Define a Shape parent class with a sides attribute initialized to 0.
  2. Create a Square child class that inherits Shape and sets sides to 4 in its constructor.
  3. Override Shape’s area() method in Square to return the area of a square.
  4. Print the area() and sides to verify overrides.

Leveraging Multiple Inheritance

Python supports inheriting from multiple parent classes through multiple inheritance. This provides even more code reuse flexibility.

The child class specifies all parent classes separated by commas:

class Parent1:
    # parent 1 definition

class Parent2:
    # parent 2 definition

class Child(Parent1, Parent2):
    # inherits from multiple parents

The first parent listed has higher precedence if attributes conflict.

Let’s see an example of multiple inheritance combining SwimFish and FlyBird Mixins with a core FlyingFish class:

class SwimFish:
    def swim(self):
        print("Swimming!")

class FlyBird:
    def fly(self):
        print("Flying!")

class FlyingFish(SwimFish, FlyBird):
    pass # inherits swim() and fly()

ff = FlyingFish()
ff.swim() # Swimming!
ff.fly() # Flying!

This allows mixing reusable swimming and flying behaviors through multiple inheritance.

Let’s practice multiple inheritance by combining mixing classes:

Exercise:

  1. Create SpeakAnimal and RollRobot parent classes with speak() and roll() methods that print appropriate messages.
  2. Inherit from both parent classes in a SpeakingRobot child class.
  3. Create an instance and call its inherited methods to test it out.

Using Abstract Base Classes for Polymorphism

Python’s powerful polymorphism allows treating derived classes as their parent type seamlessly. This flexibility is crucial for things like containers holding elements of differing types.

We can define abstract base classes (ABCs) that specify interfaces for polymorphism using the abc module:

from abc import ABC, abstractmethod

class Shape(ABC):

    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def sides(self):
        pass

Any child class must implement the abstract methods to work properly:

class Square(Shape):

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

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

    def sides(self):
        return 4

square = Square(5)
print(square.area()) # 25

Now we can treat child classes polymorphically through the base class type:

shapes = [Square(4), Circle(3)]

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

This iterates through each shape treating them interchangeably based on their Shape type.

Let’s make an abstract class and subclasses:

Exercise:

  1. Define an abstract Animal base class with abstract speak() method.
  2. Create Cat and Dog subclasses implementing speak() to print an appropriate message.
  3. Store instances of each animal in a list and call speak() on each polymorphically.

Leveraging Duck Typing for Flexibility

Python also provides flexibility through ‘duck typing’ - the idea that if it walks and quacks like a duck, we can treat it as a duck! This allows polymorphism between classes without inheriting from a common base.

As long as the needed methods and attributes are present, Python treats unrelated objects similarly:

class Duck:
    def quack(self):
        print("Quack!")

class Radio:
    def quack(self):
        print("Quack sounds!")

def make_quack(obj):
    obj.quack()

duck = Duck()
radio = Radio()

make_quack(duck) # Quack!
make_quack(radio) # Quack sounds!

Even though Radio is not a Duck subclass, make_quack() can treat it like a duck since it has the expected quack() method. This dynamic flexibility enables loose coupling and abstraction in Python.

Let’s practice some duck typing:

Exercise:

  1. Create a Quacker class with a quack() method that prints a quack message.
  2. Define unrelated Robot and Cat classes with matching quack() methods.
  3. Pass all three classes into a let_quack() function that calls quack().

Summary

In this comprehensive guide, we explored practical techniques for implementing inheritance hierarchies and polymorphic behavior in Python:

Through hands-on examples and exercises, readers should now feel confident leveraging inheritance and polymorphism to write cleaner, more flexible, and reusable object-oriented code in Python. Mastering these core principles opens the door to effectively modeling complex domains and implementing robust systems in Python.

Conclusion

Inheritance and polymorphism are foundational pillars of object-oriented programming in Python. This guide provided a practical introduction through concrete examples and hands-on exercises for implementing inheritance hierarchies and enabling polymorphic behavior. Readers should now have a solid grasp of utilizing these techniques to produce more modular and maintainable code.

There are many additional aspects to explore such as multiple inheritance quirks, metaclass programming, abstract class registries, and optimized dispatch approaches. Start by applying the basics covered here. Get creative extending reusable parent classes through inheritance and take advantage of polymorphism for dynamic flexibility. Learn to leverage these core OOP concepts and you’ll be on your way to mastering idiomatic class design in Python!