Skip to main content
โšก Calmops

Python Inheritance: Mastering Single and Multiple Inheritance

Python Inheritance: Mastering Single and Multiple Inheritance

Introduction

Imagine you’re building a game with different character types: warriors, mages, and archers. Each has common properties like health and name, but also unique abilities. Without inheritance, you’d duplicate code for each character type. With inheritance, you create a base Character class and let warriors, mages, and archers inherit from it.

Inheritance is one of the pillars of object-oriented programming. It allows you to create a hierarchy of classes where child classes inherit properties and methods from parent classes. This promotes code reuse, reduces duplication, and makes your code more maintainable.

Python supports two main inheritance patterns: single inheritance (a class inherits from one parent) and multiple inheritance (a class inherits from multiple parents). In this guide, we’ll explore both patterns, understand how Python resolves methods in complex hierarchies, and learn when to use each approach.


Part 1: Understanding Inheritance

What Is Inheritance?

Inheritance is a mechanism where a new class (child/subclass) inherits properties and methods from an existing class (parent/superclass). The child class can:

  • Use inherited methods and properties
  • Override inherited methods with its own implementation
  • Add new methods and properties

Why Use Inheritance?

Inheritance provides several benefits:

  • Code reuse: Write common code once in the parent class
  • Logical organization: Group related classes in a hierarchy
  • Polymorphism: Use objects of different types interchangeably
  • Maintainability: Changes to parent class automatically apply to children
  • Extensibility: Easily add new classes by extending existing ones

Part 2: Single Inheritance

Single inheritance is when a class inherits from exactly one parent class. It’s the simplest and most common inheritance pattern.

Basic Single Inheritance Syntax

# Parent class (superclass)
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        print(f"{self.name} makes a sound")

# Child class (subclass) inherits from Animal
class Dog(Animal):
    def speak(self):  # Override parent method
        print(f"{self.name} barks: Woof!")

# Create and use objects
dog = Dog("Buddy")
dog.speak()  # Output: Buddy barks: Woof!

Calling Parent Methods with super()

The super() function lets you call methods from the parent class:

class Vehicle:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
    
    def describe(self):
        return f"{self.brand} {self.model}"

class Car(Vehicle):
    def __init__(self, brand, model, doors):
        super().__init__(brand, model)  # Call parent's __init__
        self.doors = doors
    
    def describe(self):
        # Call parent's describe and add more info
        parent_description = super().describe()
        return f"{parent_description} with {self.doors} doors"

car = Car("Toyota", "Camry", 4)
print(car.describe())  # Output: Toyota Camry with 4 doors

Practical Example: Employee Hierarchy

class Employee:
    """Base class for all employees"""
    
    def __init__(self, name, employee_id, salary):
        self.name = name
        self.employee_id = employee_id
        self.salary = salary
    
    def get_info(self):
        return f"Name: {self.name}, ID: {self.employee_id}, Salary: ${self.salary}"
    
    def give_raise(self, amount):
        self.salary += amount
        print(f"{self.name} received a raise of ${amount}")

class Manager(Employee):
    """Manager inherits from Employee"""
    
    def __init__(self, name, employee_id, salary, department):
        super().__init__(name, employee_id, salary)
        self.department = department
        self.team = []
    
    def add_team_member(self, employee):
        self.team.append(employee)
        print(f"{employee.name} added to {self.name}'s team")
    
    def get_info(self):
        base_info = super().get_info()
        return f"{base_info}, Department: {self.department}, Team Size: {len(self.team)}"

class Developer(Employee):
    """Developer inherits from Employee"""
    
    def __init__(self, name, employee_id, salary, programming_language):
        super().__init__(name, employee_id, salary)
        self.programming_language = programming_language
    
    def get_info(self):
        base_info = super().get_info()
        return f"{base_info}, Language: {self.programming_language}"

# Create and use objects
manager = Manager("Alice", "M001", 100000, "Engineering")
dev1 = Developer("Bob", "D001", 80000, "Python")
dev2 = Developer("Charlie", "D002", 85000, "JavaScript")

manager.add_team_member(dev1)
manager.add_team_member(dev2)

print(manager.get_info())
# Output: Name: Alice, ID: M001, Salary: $100000, Department: Engineering, Team Size: 2

print(dev1.get_info())
# Output: Name: Bob, ID: D001, Salary: $80000, Language: Python

manager.give_raise(10000)
# Output: Alice received a raise of $10000

Checking Inheritance Relationships

class Animal:
    pass

class Dog(Animal):
    pass

dog = Dog()

# Check if dog is an instance of Dog
print(isinstance(dog, Dog))      # Output: True

# Check if dog is an instance of Animal (parent class)
print(isinstance(dog, Animal))   # Output: True

# Check if Dog is a subclass of Animal
print(issubclass(Dog, Animal))   # Output: True

# Get the parent class
print(Dog.__bases__)             # Output: (<class 'Animal'>,)

Part 3: Multiple Inheritance

Multiple inheritance allows a class to inherit from multiple parent classes. While powerful, it requires careful design to avoid complexity.

Basic Multiple Inheritance Syntax

# Parent class 1
class Swimmer:
    def swim(self):
        print("Swimming...")

# Parent class 2
class Flyer:
    def fly(self):
        print("Flying...")

# Child class inherits from both parents
class Duck(Swimmer, Flyer):
    def quack(self):
        print("Quack!")

# Create and use object
duck = Duck()
duck.swim()   # Output: Swimming...
duck.fly()    # Output: Flying...
duck.quack()  # Output: Quack!

Method Resolution Order (MRO)

When a class inherits from multiple parents, Python needs to know which parent’s method to use if both have the same method. This is determined by the Method Resolution Order (MRO).

Python uses the C3 Linearization algorithm to determine MRO. You can view it using:

class A:
    def method(self):
        print("A's method")

class B(A):
    def method(self):
        print("B's method")

class C(A):
    def method(self):
        print("C's method")

class D(B, C):
    pass

# View the MRO
print(D.mro())
# Output: [<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>]

# Or use __mro__
print(D.__mro__)

# Use it
d = D()
d.method()  # Output: B's method (B comes before C in MRO)

MRO Rules:

  1. Child classes are checked before parents
  2. Parents are checked in the order they’re listed in the class definition
  3. Each class appears only once in the MRO

The Diamond Problem

The diamond problem occurs when a class inherits from two classes that both inherit from the same parent:

#     Animal
#    /      \
#   Dog    Cat
#    \      /
#    Pet

class Animal:
    def speak(self):
        print("Animal speaks")

class Dog(Animal):
    def speak(self):
        print("Dog barks")

class Cat(Animal):
    def speak(self):
        print("Cat meows")

class Pet(Dog, Cat):
    pass

pet = Pet()
pet.speak()  # Output: Dog barks

# Check MRO to understand why
print(Pet.mro())
# Output: [<class 'Pet'>, <class 'Dog'>, <class 'Cat'>, <class 'Animal'>, <class 'object'>]

Python’s C3 linearization algorithm solves the diamond problem by ensuring:

  • Each class appears only once
  • Parent order is preserved
  • Child classes are checked before parents

Practical Example: Vehicle System

class Vehicle:
    """Base vehicle class"""
    def __init__(self, brand):
        self.brand = brand
    
    def start(self):
        print(f"{self.brand} vehicle starting...")

class Electric:
    """Mixin for electric vehicles"""
    def charge(self):
        print("Charging battery...")
    
    def get_battery_level(self):
        return "80%"

class Gasoline:
    """Mixin for gasoline vehicles"""
    def refuel(self):
        print("Refueling tank...")
    
    def get_fuel_level(self):
        return "3/4 tank"

class HybridCar(Vehicle, Electric, Gasoline):
    """Hybrid car inherits from Vehicle and both fuel types"""
    
    def __init__(self, brand, model):
        super().__init__(brand)
        self.model = model
    
    def get_status(self):
        return f"{self.brand} {self.model} - Battery: {self.get_battery_level()}, Fuel: {self.get_fuel_level()}"

# Create and use hybrid car
hybrid = HybridCar("Toyota", "Prius")
hybrid.start()           # Output: Toyota vehicle starting...
hybrid.charge()          # Output: Charging battery...
hybrid.refuel()          # Output: Refueling tank...
print(hybrid.get_status())
# Output: Toyota Prius - Battery: 80%, Fuel: 3/4 tank

# Check MRO
print(HybridCar.mro())
# Output: [<class 'HybridCar'>, <class 'Vehicle'>, <class 'Electric'>, <class 'Gasoline'>, <class 'object'>]

Using super() with Multiple Inheritance

class A:
    def method(self):
        print("A's method")

class B(A):
    def method(self):
        print("B's method")
        super().method()

class C(A):
    def method(self):
        print("C's method")
        super().method()

class D(B, C):
    def method(self):
        print("D's method")
        super().method()

# Create and use
d = D()
d.method()
# Output:
# D's method
# B's method
# C's method
# A's method

This demonstrates how super() follows the MRO, calling each class in order.


Part 4: Mixins and Composition

Using Mixins

Mixins are classes designed to provide specific functionality to be mixed into other classes. They’re not meant to stand alone.

class TimestampMixin:
    """Mixin to add timestamp functionality"""
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.created_at = None
        self.updated_at = None
    
    def set_timestamp(self):
        from datetime import datetime
        self.updated_at = datetime.now()
        if self.created_at is None:
            self.created_at = datetime.now()

class SerializableMixin:
    """Mixin to add serialization functionality"""
    def to_dict(self):
        return self.__dict__
    
    def to_json(self):
        import json
        return json.dumps(self.to_dict(), default=str)

class User(TimestampMixin, SerializableMixin):
    def __init__(self, name, email):
        super().__init__()
        self.name = name
        self.email = email
        self.set_timestamp()

# Create and use
user = User("Alice", "[email protected]")
print(user.to_dict())
# Output: {'name': 'Alice', 'email': '[email protected]', 'created_at': datetime(...), 'updated_at': datetime(...)}

print(user.to_json())
# Output: {"name": "Alice", "email": "[email protected]", "created_at": "2025-12-16 10:30:00.123456", "updated_at": "2025-12-16 10:30:00.123456"}

Composition vs Inheritance

Sometimes composition (using objects as attributes) is better than inheritance:

# Bad: Inheritance when composition is better
class Car(Engine, Transmission, Wheels):
    pass

# Good: Composition
class Engine:
    def start(self):
        print("Engine starting...")

class Transmission:
    def shift(self, gear):
        print(f"Shifting to {gear}")

class Car:
    def __init__(self):
        self.engine = Engine()
        self.transmission = Transmission()
    
    def start(self):
        self.engine.start()
    
    def drive(self):
        self.transmission.shift("D")
        print("Car driving...")

car = Car()
car.start()   # Output: Engine starting...
car.drive()   # Output: Shifting to D, Car driving...

When to use composition:

  • Objects have a “has-a” relationship (Car has an Engine)
  • You want to avoid deep inheritance hierarchies
  • You need flexibility in combining behaviors

When to use inheritance:

  • Objects have an “is-a” relationship (Dog is an Animal)
  • You want to create a logical hierarchy
  • Child classes truly specialize parent classes

Part 5: Best Practices and Pitfalls

Best Practice 1: Keep Hierarchies Shallow

# Bad: Deep hierarchy
class A:
    pass

class B(A):
    pass

class C(B):
    pass

class D(C):
    pass

# Good: Flatter hierarchy
class Animal:
    pass

class Dog(Animal):
    pass

class Poodle(Dog):
    pass

Best Practice 2: Use Abstract Base Classes

from abc import ABC, abstractmethod

class Shape(ABC):
    """Abstract base class for shapes"""
    
    @abstractmethod
    def area(self):
        pass
    
    @abstractmethod
    def perimeter(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14 * self.radius ** 2
    
    def perimeter(self):
        return 2 * 3.14 * self.radius

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)

# Can't instantiate abstract class
# shape = Shape()  # TypeError

# Can instantiate concrete classes
circle = Circle(5)
print(circle.area())  # Output: 78.5

Best Practice 3: Avoid Multiple Inheritance Complexity

# Avoid: Complex multiple inheritance
class A:
    pass

class B(A):
    pass

class C(A):
    pass

class D(B, C):
    pass

class E(D, B):  # Confusing!
    pass

# Better: Use composition or simpler hierarchy
class Component:
    pass

class Logger(Component):
    def log(self, message):
        print(message)

class Database(Component):
    def query(self, sql):
        print(f"Executing: {sql}")

class Application:
    def __init__(self):
        self.logger = Logger()
        self.database = Database()

Pitfall 1: Forgetting to Call super().__init__()

# Bad: Parent's __init__ not called
class Parent:
    def __init__(self, name):
        self.name = name

class Child(Parent):
    def __init__(self, name, age):
        self.age = age
        # Forgot super().__init__(name)

child = Child("Alice", 10)
print(child.name)  # AttributeError: 'Child' object has no attribute 'name'

# Good: Call super().__init__()
class Child(Parent):
    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

child = Child("Alice", 10)
print(child.name)  # Output: Alice

Pitfall 2: Ambiguous Method Resolution

# Problematic: Ambiguous MRO
class A:
    def method(self):
        print("A")

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

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

# This will raise TypeError if not careful
# class D(B, C, A):  # Bad: A appears twice
#     pass

# Good: Let Python determine MRO
class D(B, C):
    pass

d = D()
d.method()  # Output: B (follows MRO)

Pitfall 3: Overriding Without Calling Parent

# Bad: Loses parent functionality
class Logger:
    def log(self, message):
        print(f"[LOG] {message}")

class FileLogger(Logger):
    def log(self, message):
        # Forgot to call parent
        with open("log.txt", "a") as f:
            f.write(message + "\n")

# Now console logging is lost!

# Good: Call parent method
class FileLogger(Logger):
    def log(self, message):
        super().log(message)  # Console logging
        with open("log.txt", "a") as f:
            f.write(message + "\n")  # File logging

Part 6: Comparison: Single vs Multiple Inheritance

Aspect Single Inheritance Multiple Inheritance
Complexity Simple, easy to understand Complex, harder to debug
MRO Straightforward Requires understanding C3 linearization
Use Cases Most common scenarios Mixins, multiple interfaces
Maintenance Easier to maintain Requires careful design
Performance Slightly faster Slightly slower (more lookups)
Learning Curve Beginner-friendly Intermediate to advanced

Conclusion

Inheritance is a powerful tool for building organized, reusable code. Single inheritance is the most common pattern and should be your default choice. Multiple inheritance is useful for mixins and combining multiple interfaces, but requires careful design to avoid complexity.

Key takeaways:

  • Single inheritance is simpler and should be your first choice
  • Multiple inheritance enables powerful patterns but requires understanding MRO
  • Use super() to call parent methods and follow the MRO
  • Understand the diamond problem and how Python solves it with C3 linearization
  • Prefer composition over inheritance when objects have a “has-a” relationship
  • Use abstract base classes to define interfaces
  • Keep hierarchies shallow to maintain simplicity
  • Avoid ambiguous MRO by designing clear inheritance structures

Start with single inheritance to build solid foundations. As you gain experience, you’ll recognize when multiple inheritance or composition is appropriate. Remember: the best code is code that’s easy to understand and maintain. Choose inheritance patterns that serve that goal.

Happy coding!

Comments