Inheritance lets one class reuse and extend another. Python supports multiple inheritance, which gives you great power and great responsibility. The Method Resolution Order (MRO) decides which parent's method runs when multiple match. This lesson covers single and multiple inheritance, super(), abstract base classes, mixins, and the design heuristic that inheritance should model an is-a relationship.
Single inheritance
A subclass inherits attributes and methods from its parent. The subclass can override methods, extend the parent's __init__ with super(), and add new behaviour of its own.
class Animal:
def __init__(self, name: str):
self.name = name
def speak(self) -> str:
return "..."
class Dog(Animal):
def speak(self) -> str:
return "Woof"
class Cat(Animal):
def speak(self) -> str:
return "Meow"
for pet in [Dog("Rex"), Cat("Whiskers")]:
print(f"{pet.name}: {pet.speak()}")
super() in single inheritance
Use super() to call a method on the parent. In __init__, the common pattern is to let the parent initialize its own state, then add subclass-specific state. This avoids duplicating the parent's setup.
class Vehicle:
def __init__(self, make: str, year: int):
self.make = make
self.year = year
class Car(Vehicle):
def __init__(self, make: str, year: int, doors: int):
super().__init__(make, year)
self.doors = doors
c = Car("Honda", 2024, 4)
print(c.make, c.year, c.doors) # Honda 2024 4
Multiple inheritance and the MRO
A class can inherit from more than one parent. Python uses the C3 linearization algorithm to compute a deterministic MRO. Every class has an __mro__ tuple you can inspect to see the lookup order.
class A: def who(self): return "A"
class B(A): def who(self): return "B"
class C(A): def who(self): return "C"
class D(B, C): pass
print(D().who()) # B
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>,
# <class '__main__.A'>, <class 'object'>)
Mixins for shared behaviour
A mixin is a small class that provides a single piece of behaviour. Combine mixins with a primary class to compose capabilities. The order matters: the mixin should appear before the primary class so its methods win on the MRO when intended.
class JsonMixin:
def to_json(self) -> str:
import json
return json.dumps(self.__dict__, default=str)
class ReprMixin:
def __repr__(self) -> str:
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{self.__class__.__name__}({attrs})"
class User(ReprMixin, JsonMixin):
def __init__(self, name: str, age: int):
self.name, self.age = name, age
u = User("Asha", 28)
print(u) # User(name='Asha', age=28)
print(u.to_json()) # {"name": "Asha", "age": 28}
Abstract base classes
An abstract base class (ABC) defines methods that subclasses must implement. You cannot instantiate the ABC itself. Use abc.ABC and the @abstractmethod decorator to enforce the contract.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2
def perimeter(self) -> float:
return 4 * self.side
print(Square(3).area()) # 9
# Shape() # TypeError: Can't instantiate abstract class
Prefer composition when in doubt
Inheritance models an is-a relationship. Composition models a has-a relationship. When a class merely uses another class's behaviour, prefer to hold an instance of it as an attribute rather than inherit. The rule of thumb: reach for inheritance only when every subclass really is a kind of the parent.
# Composition: Car has an Engine, not is-a Engine
class Engine:
def start(self) -> str:
return "engine started"
class Car:
def __init__(self):
self.engine = Engine()
def start(self) -> str:
return f"Car {self.engine.start()}"
print(Car().start()) # Car engine started
Common pitfalls with inheritance
Three traps catch developers new to inheritance. First, super() in multiple inheritance is more subtle than it looks. If two parents both call super().__init__, their methods run in MRO order, not source order, which can leave some attributes unset if you do not plan the chain. Always have every class in the hierarchy call super().__init__ (or document why it does not), and pass through the arguments the parent expects. Second, diamond inheritance with state-sharing mutable class attributes causes subtle data corruption. Every class in the diamond shares the same attribute by default, so changing it in one branch changes it everywhere. Use instance attributes (set in __init__) to keep state per object. Third, deep hierarchies (more than three levels) accumulate accidental coupling. Tests for a grandchild break when its grandparent changes. Flatten with composition or replace with a small set of mixins.
Comparison with JavaScript and C++ inheritance
If you come from JavaScript, Python's class-based inheritance is closer to Java or C# than to JS's prototype model. JavaScript has only single inheritance through class extends, with reusable behaviour arriving via mixins added to the prototype. Python gives you multiple inheritance directly and lets you control method resolution through the MRO. C++ has full multiple inheritance with virtual base classes for the diamond case. The C++ approach is more rigid but also more explicit: the diamond must be declared, and the compiler picks the path. Python's C3 linearisation is algorithmically simpler to use but harder to predict by inspection when the hierarchy gets deep. If you are translating a C++ diamond to Python, write the MRO down on paper before you write the classes.
Real-world example: a small plugin system
Mixins and ABCs make a clean plugin system. Define a base class that captures the protocol, mix in behaviours like logging and persistence, and let concrete plugins inherit the result. Here is a minimal example that backs a report with a JSON dump and a string repr.
from abc import ABC, abstractmethod
class ReportMixin:
def describe(self) -> str:
return f"{self.__class__.__name__}(rows={len(self.rows)})"
class JsonMixin:
def to_json(self) -> str:
import json
return json.dumps(self.rows, default=str)
class ReportBase(ReportMixin, JsonMixin):
def __init__(self, rows: list[dict]):
self.rows = rows
@abstractmethod
def render(self) -> str: ...
class SimpleReport(ReportBase):
def render(self) -> str:
return "
".join(str(r) for r in self.rows)
r = SimpleReport([{"id": 1, "name": "Asha"}, {"id": 2, "name": "Bala"}])
print(r.describe())
print(r.to_json())
The mixins compose neatly because each adds a single capability. Add a HtmlMixin or a CsvFileMixin later without touching the existing classes. That is the payoff for keeping hierarchies shallow and behaviours small.
Summary
Inheritance shares behaviour across an is-a hierarchy. super() lets you cooperate with the parent. The MRO is deterministic, inspect it via __mro__. Mixins compose single-purpose behaviour. ABCs enforce contracts. Prefer composition when the relationship is has-a, not is-a. The next lesson covers exceptions, the with statement, and reading and writing files.