Inheritance
Inheritance lets one class build on top of another — reusing its attributes and methods without rewriting them. The class being inherited from is called the parent (or base class), and the class that inherits is called the child (or subclass).
The core idea is the is-a relationship: a Dog is an Animal. A Manager is an Employee. When that
relationship holds, inheritance is the right tool.
Animal ← parent: defines shared behaviour├── Dog ← child: inherits from Animal, adds its own└── Cat ← child: inherits from Animal, adds its ownThis gives you two things:
- Reuse — the child gets everything the parent has for free
- Polymorphism — you can treat a
Dogand aCatasAnimalobjects and callspeak()on both, without caring which one you have
class Animal: def __init__(self, name): self.name = name
def speak(self): raise NotImplementedError("Subclass must implement speak()")
def eat(self): return f"{self.name} is eating."
class Dog(Animal): # Dog inherits from Animal def speak(self): # Override parent method return f"{self.name} says Woof!"
class Cat(Animal): def speak(self): return f"{self.name} says Meow!"
dog = Dog("Rex")cat = Cat("Whiskers")
print(dog.speak()) # Rex says Woof!print(cat.speak()) # Whiskers says Meow!print(dog.eat()) # Rex is eating. ← inherited from Animal
# Check relationshipsprint(isinstance(dog, Dog)) # Trueprint(isinstance(dog, Animal)) # True ← dog IS an Animalprint(issubclass(Dog, Animal)) # True:::note[Polymorphism] Here’s a definition to add right after you mention it:
Polymorphism — from Greek poly (many) + morphe (form). The ability of different objects to respond to the same method call in their own way. You call
speak()on anyAnimaland each subclass decides what that means — aDogbarks, aCatmeows. The caller doesn’t need to know which one it has.
Or a more code-grounded version if you prefer:
# Polymorphism in action — same call, different behaviouranimals = [Dog("Rex"), Cat("Whiskers"), Dog("Buddy")]
for animal in animals: print(animal.speak()) # each responds in its own way# Rex says Woof!# Whiskers says Meow!# Buddy says Woof!The power is in the loop — you never check if isinstance(animal, Dog). You just call speak() and trust each object
to do the right thing. :::
super() — Calling the Parent
Section titled “super() — Calling the Parent”When a child class defines its own __init__, it replaces the parent’s, which means the parent’s setup code never
runs unless you explicitly call it. That’s what super() is for.
super() gives you a reference to the parent class so you can call its methods from inside the child. The most common
use is in __init__ to run the parent’s initializer first to set up the shared attributes, then add the
child-specific ones on top.
Dog.__init__ runs → super().__init__(name, age) ← delegates to Animal first → self.name = name ← Animal sets shared attributes → self.age = age → self.breed = breed ← Dog adds its own:::danger[Don’t forget super()] A child class that defines __init__ without calling super().__init__() silently
skips the parent’s setup — leading to missing attributes and confusing errors that are hard to debug. :::
Here is a full inheritance example with Animal, Dog and Cat:
class Animal: def __init__(self, name, age): self.name = name self.age = age
class Dog(Animal): def __init__(self, name, age, breed): super().__init__(name, age) # Call Animal's __init__ self.breed = breed # Add Dog-specific attribute
def info(self): return f"{self.name} ({self.breed}), age {self.age}"
dog = Dog("Rex", 3, "Labrador")print(dog.info()) # Rex (Labrador), age 3Multiple Inheritance & MRO
Section titled “Multiple Inheritance & MRO”Python allows a class to inherit from multiple parents at once. This is powerful but raises an immediate question: if both parents define the same method, which one wins?
Python answers this with the MRO — Method Resolution Order — a deterministic lookup chain that defines exactly which class Python searches first when resolving a method call. It is computed using the C3 linearization algorithm and always follows a left-to-right, depth-first order that respects the inheritance hierarchy.
In the example below, D inherits from both B and C, which both inherit from A. When d.hello() is called,
Python walks the MRO chain — D → B → C → A — and uses the first match it finds:
d.hello() → check D — no hello() → check B — found! returns "Hello from B" ✓You can always inspect the chain with D.__mro__ — a good habit when debugging unexpected method resolution in complex
hierarchies.
class A: def hello(self): return "Hello from A"
class B(A): def hello(self): return "Hello from B"
class C(A): def hello(self): return "Hello from C"
class D(B, C): # Inherits from both B and C pass
d = D()print(d.hello()) # Hello from B
# MRO = Method Resolution Order — the lookup chainprint(D.__mro__) # D → B → C → A → object▶ Run this example
Python uses C3 linearization to determine which method wins. Always check __mro__ when using multiple inheritance.
MRO — Method Resolution Order
Section titled “MRO — Method Resolution Order”Python answers this with the MRO — Method Resolution Order — a deterministic lookup chain computed using the C3 linearization algorithm. The rules are simple:
- A child is always checked before its parents
- When inheriting from multiple parents, they are checked left to right in the order you declare them —
class D(B, C)means B is checked before C - A class is never checked before all its subclasses have been checked first
The inheritance tree for this example looks like this:
object │ A / \ B C \ / DPython flattens this diamond into a linear lookup chain from bottom to top, left to right:
D → B → C → A → objectSo for d.hello():
d.hello() → D no hello() defined → B found! → "Hello from B" ✓ → C never reached → A never reachedThis guarantees that the lookup order is always predictable and that no class is ever skipped or visited twice.
Quick reference
Section titled “Quick reference”A summary of the inheritance concepts covered in this chapter.
Inheritance
Section titled “Inheritance”| Concept | Syntax | Purpose |
|---|---|---|
| Inherit from a parent | class Dog(Animal): | Reuse parent attributes and methods |
| Override a method | Redefine it in the child | Replace parent behaviour |
| Call the parent | super().method() | Extend rather than replace |
| Check instance | isinstance(dog, Animal) | True if dog is an Animal or subclass |
| Check subclass | issubclass(Dog, Animal) | True if Dog inherits from Animal |
Multiple inheritance
Section titled “Multiple inheritance”| Concept | Syntax | Purpose |
|---|---|---|
| Multiple parents | class D(B, C): | Inherit from both B and C |
| Inspect MRO | D.__mro__ | See the full lookup chain |
| MRO order | left to right, child before parent | Determines which method wins |
When to use what
Section titled “When to use what”| Situation | Use |
|---|---|
| Child shares behaviour with parent | Inheritance |
| Child needs different behaviour | Override the method |
Child extends parent __init__ | super().__init__() |
| Same method exists in multiple parents | Check __mro__ to understand which wins |
| Checking type at runtime | isinstance() over type() |
Exercises
Section titled “Exercises”Exercise 1 — Inheritance Intermediate
Section titled “Exercise 1 — Inheritance Intermediate”Model a company’s staff. Create:
Employeebase class withname,salary, andget_pay()methodFullTimeEmployee(Employee)— fixed monthly salaryPartTimeEmployee(Employee)— hourly rate × hours workedManager(Employee)— salary + sum of bonuses list- All should have meaningful
__str__ - A standalone function
print_payroll(employees)that prints each employee’s pay
staff = [ FullTimeEmployee("Alice", 5000), PartTimeEmployee("Bob", 20, 80), # $20/hr × 80hrs Manager("Carol", 7000, [500, 300]), # salary + bonuses]print_payroll(staff)# Alice (FullTime) — $5000# Bob (PartTime) — $1600# Carol (Manager) — $7800Solution
class Employee: def __init__(self, name, salary): self.name = name self.salary = salary
def get_pay(self): return self.salary
def __str__(self): return f"Employee(name={self.name}, salary={self.salary}"
class FullTimeEmployee(Employee): def __str__(self): return f"{self.name} (FullTime) - ${self.salary}"
class Manager(Employee): def __init__(self, name, salary, bonuses: list[int]): super().__init__(name, salary + sum(bonuses))
def __str__(self): return f"{self.name} (Manager) - ${self.salary}"
class PartTimeEmployee(Employee): def __init__(self, name, salaryPerHour, workedHours): super().__init__(name, salaryPerHour * workedHours)
def __str__(self): return f"{self.name} (PartTime) - ${self.salary}"
def print_payroll(employees: list[Employee]): for e in employees: print(e)from exercises.ch01_oop.inheritance.exercise_01 import ( FullTimeEmployee, Manager, PartTimeEmployee, print_payroll,)
def test_utils(capsys): staff = [ FullTimeEmployee("Alice", 5000), PartTimeEmployee("Bob", 20, 80), # $20/hr × 80hrs Manager("Carol", 7000, [500, 300]), # salary + bonuses ]
print_payroll(staff)
# test what is printed, capsys is injected by pytest automatically captured = capsys.readouterr() assert captured.out == ( "Alice (FullTime) - $5000\nBob (PartTime) - $1600\nCarol (Manager) - $7800\n" )Exercise 2 — Mixin classes Advanced
Section titled “Exercise 2 — Mixin classes Advanced”Create a set of mixin classes and combine them:
SerializeMixin— addsto_dict()andto_json()methodsValidateMixin— addsvalidate()that checks all required fields are non-emptyTimestampMixin— addscreated_atandupdated_at, and anupdate()method that refreshesupdated_atReprMixin— auto-generates__repr__from instance__dict__- A
Userclass that inherits all four mixins
u = User(name="Alice", email="alice@example.com", age=30)print(u.validate()) # Trueprint(u.to_dict()) # {'name': 'Alice', 'email': '...', 'age': 30}print(u.to_json()) # '{"name": "Alice", ...}'print(u.created_at) # 2026-03-22 ...u.update(email="new@x.com")print(u.updated_at) # Updated timestampprint(repr(u)) # User(name=Alice, email=new@x.com, age=30)