This article is part of in the series
Published: Friday 16th May 2025

python inheritance

In the world of object-oriented programming, inheritance stands as a fundamental pillar that enables code reuse, promotes logical organization, and supports elegant software design. Python, with its clean syntax and flexible approach to OOP, offers a particularly accessible yet powerful implementation of inheritance. This article explores the concept of inheritance in Python, covering basic principles, advanced techniques, common patterns, and best practices to help you write more maintainable and efficient code.

Understanding the Basics of Inheritance in Python

At its core, inheritance allows a class (called a subclass or derived class) to inherit attributes and methods from another class (called a superclass or base class). This creates a parent-child relationship between classes, establishing a hierarchy that models "is-a" relationships.

Basic Syntax and Implementation

In Python, creating a subclass is straightforward:

class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        pass  # This will be implemented by subclasses

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

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

# Creating instances
dog = Dog("Rex")
cat = Cat("Whiskers")

print(dog.speak())  # Output: Rex says Woof!
print(cat.speak())  # Output: Whiskers says Meow!

In this example, Dog and Cat inherit from Animal, gaining access to its name attribute while providing their own implementations of the speak method.

The super() Function

When a subclass needs to extend (rather than completely replace) the functionality of its parent class, the super() function comes into play:

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def info(self):
        return f"{self.name} is {self.age} years old"

class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)  # Call parent's __init__
        self.breed = breed
    
    def info(self):
        base_info = super().info()  # Call parent's info method
        return f"{base_info} and is a {self.breed}"

dog = Dog("Rex", 3, "German Shepherd")
print(dog.info())  # Output: Rex is 3 years old and is a German Shepherd

The super() function provides a reference to the parent class, allowing you to call its methods. This is particularly useful in constructors and when overriding methods.

Types of Inheritance in Python

Python supports various inheritance patterns to model different kinds of relationships:

Single Inheritance

The simplest form, where a subclass inherits from a single superclass, as shown in the previous examples.

Multiple Inheritance

One of Python's powerful features is its support for multiple inheritance, where a class can inherit from multiple parent classes:

class Flying:
    def fly(self):
        return "I can fly!"

class Swimming:
    def swim(self):
        return "I can swim!"

class Duck(Flying, Swimming):
    def speak(self):
        return "Quack!"

duck = Duck()
print(duck.fly())    # Output: I can fly!
print(duck.swim())   # Output: I can swim!
print(duck.speak())  # Output: Quack!

Multiple inheritance allows a class to combine behaviors from different sources, though it should be used judiciously to avoid complexity.

Multilevel Inheritance

In multilevel inheritance, a class inherits from a subclass, creating a "grandparent" relationship:

class Animal:
    def breathe(self):
        return "I breathe oxygen"

class Mammal(Animal):
    def feed_young(self):
        return "I feed my young with milk"

class Dog(Mammal):
    def bark(self):
        return "Woof!"

dog = Dog()
print(dog.breathe())     # From Animal
print(dog.feed_young())  # From Mammal
print(dog.bark())        # From Dog

This creates deeper hierarchies that represent increasingly specific categories.

Hierarchical Inheritance

When multiple subclasses inherit from the same parent class:

class Vehicle:
    def __init__(self, brand):
        self.brand = brand

class Car(Vehicle):
    def type(self):
        return "I am a car"

class Motorcycle(Vehicle):
    def type(self):
        return "I am a motorcycle"

# Both inherit from Vehicle
car = Car("Toyota")
motorcycle = Motorcycle("Harley-Davidson")

Hybrid Inheritance

A combination of multiple inheritance types, often seen in complex systems:

class A:
    def method_a(self):
        return "Method A"

class B(A):
    def method_b(self):
        return "Method B"

class C(A):
    def method_c(self):
        return "Method C"

class D(B, C):
    def method_d(self):
        return "Method D"

# D inherits from B and C, which both inherit from A

Method Resolution Order (MRO)

When dealing with multiple inheritance, understanding how Python resolves method calls becomes crucial. Python uses the C3 linearization algorithm to determine the Method Resolution Order (MRO).

class A:
    def who_am_i(self):
        return "I am A"

class B(A):
    def who_am_i(self):
        return "I am B"

class C(A):
    def who_am_i(self):
        return "I am C"

class D(B, C):
    pass

d = D()
print(d.who_am_i())  # Output: I am B
print(D.__mro__)     # Shows the resolution order

The MRO determines which method gets called when the same method exists in multiple parent classes. You can inspect the MRO using the __mro__ attribute or the mro() method.

Advanced Inheritance Techniques

Abstract Base Classes

Abstract base classes (ABCs) define interfaces that derived classes must implement:

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)

# This would raise an error if area or perimeter weren't implemented
rect = Rectangle(5, 10)

ABCs ensure that subclasses provide implementations for required methods, helping to enforce consistent interfaces.

Mixins

Mixins are classes designed to provide additional functionality to other classes through multiple inheritance:

class JSONSerializableMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

class Person(JSONSerializableMixin):
    def __init__(self, name, age):
        self.name = name
        self.age = age

person = Person("Alice", 30)
print(person.to_json())  # Output: {"name": "Alice", "age": 30}

They also  provide a way to "mix in" functionality without creating deep inheritance hierarchies.

Properties and Inheritance

Properties allow controlled access to attributes and work well with inheritance:

class Person:
    def __init__(self, name):
        self._name = name
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError("Name must be a string")
        self._name = value

class Employee(Person):
    def __init__(self, name, employee_id):
        super().__init__(name)
        self.employee_id = employee_id

# Employee inherits the name property with its validation

Best Practices and Common Patterns

Favor Composition Over Inheritance

While inheritance is powerful, composition (creating relationships through object references) often provides more flexibility:

# Instead of inheritance:
class Car(Vehicle):
    pass

# Consider composition:
class Car:
    def __init__(self):
        self.engine = Engine()
        self.wheels = [Wheel() for _ in range(4)]

This approach can lead to more maintainable code, especially in complex systems.

Keep Inheritance Hierarchies Shallow

Deep inheritance hierarchies can become difficult to understand and maintain. Try to keep your hierarchies no more than two or three levels deep when possible.

Use Inheritance for "Is-A" Relationships

Inheritance should model an "is-a" relationship (a Dog is an Animal), while composition should model "has-a" relationships (a Car has an Engine).

Design for Inheritance or Prohibit It

Classes should either be designed for inheritance (with documented extension points) or explicitly prevent it. In Python 3.8+, you can use:

class FinalClass(final=True):
    """This class cannot be subclassed."""
    pass

For earlier Python versions, create a metaclass or document the intent clearly.

Common Inheritance Patterns in Python

The Template Method Pattern

Define the skeleton of an algorithm in a method, deferring some steps to subclasses:

class DataProcessor:
    def process(self, data):
        clean_data = self.clean(data)
        processed_data = self.transform(clean_data)
        return self.format(processed_data)
    
    def clean(self, data):
        # Default implementation
        return data
    
    def transform(self, data):
        # Subclasses must implement this
        raise NotImplementedError
    
    def format(self, data):
        # Default implementation
        return data

class CSVProcessor(DataProcessor):
    def transform(self, data):
        # Specific implementation for CSV
        return [row.split(',') for row in data]

The Strategy Pattern

While often implemented through composition, inheritance can also be used:

class SortingAlgorithm:
    def sort(self, data):
        pass

class QuickSort(SortingAlgorithm):
    def sort(self, data):
        # Implement quicksort
        return sorted(data)  # Simplified for example

class MergeSort(SortingAlgorithm):
    def sort(self, data):
        # Implement mergesort
        return sorted(data)  # Simplified for example

Common Pitfalls and How to Avoid Them

Diamond Problem

The diamond problem occurs when a class inherits from two classes that both inherit from a common base class:

class A:
    def method(self):
        return "A"

class B(A):
    def method(self):
        return "B"

class C(A):
    def method(self):
        return "C"

class D(B, C):
    pass

# Which method() does D inherit?

Python's MRO resolves this, but it's best to avoid such complex inheritance structures when possible.

Overusing Inheritance

Inheritance creates tight coupling between classes. Consider whether interfaces, composition, or simple functions might serve your needs better.

Ignoring the Liskov Substitution Principle

Subclasses should be substitutable for their base classes without altering the correctness of the program:

# Problematic
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def set_width(self, width):
        self.width = width
    
    def set_height(self, height):
        self.height = height

class Square(Rectangle):
    def set_width(self, width):
        self.width = width
        self.height = width  # Violates expectations for a Rectangle
    
    def set_height(self, height):
        self.height = height
        self.width = height  # Violates expectations for a Rectangle

Summary

Python's implementation of inheritance provides a powerful tool for modeling relationships between classes, enabling code reuse, and creating well-structured object hierarchies. By understanding the different types of inheritance, advanced techniques like abstract base classes and mixins, and common patterns and pitfalls, you can leverage inheritance effectively in your Python projects.

Similar Articles

https://www.w3schools.com/python/python_inheritance.asp

https://realpython.com/inheritance-composition-python/

More from Python Central

https://www.pythoncentral.io/series/python-classes-tutorial/

Python DefaultDict: Efficient Dictionary Handling