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:
- Child classes are checked before parents
- Parents are checked in the order they’re listed in the class definition
- 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