Skip to content

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 own

This gives you two things:

  • Reuse — the child gets everything the parent has for free
  • Polymorphism — you can treat a Dog and a Cat as Animal objects and call speak() 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 relationships
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True ← dog IS an Animal
print(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 any Animal and each subclass decides what that means — a Dog barks, a Cat meows. 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 behaviour
animals = [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. :::


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:

Inheritance in action
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 3

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 chain
print(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.

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
\ /
D

Python flattens this diamond into a linear lookup chain from bottom to top, left to right:

D B C A object

So for d.hello():

d.hello()
D no hello() defined
B found! "Hello from B"
C never reached
A never reached

This guarantees that the lookup order is always predictable and that no class is ever skipped or visited twice.

A summary of the inheritance concepts covered in this chapter.

ConceptSyntaxPurpose
Inherit from a parentclass Dog(Animal):Reuse parent attributes and methods
Override a methodRedefine it in the childReplace parent behaviour
Call the parentsuper().method()Extend rather than replace
Check instanceisinstance(dog, Animal)True if dog is an Animal or subclass
Check subclassissubclass(Dog, Animal)True if Dog inherits from Animal
ConceptSyntaxPurpose
Multiple parentsclass D(B, C):Inherit from both B and C
Inspect MROD.__mro__See the full lookup chain
MRO orderleft to right, child before parentDetermines which method wins
SituationUse
Child shares behaviour with parentInheritance
Child needs different behaviourOverride the method
Child extends parent __init__super().__init__()
Same method exists in multiple parentsCheck __mro__ to understand which wins
Checking type at runtimeisinstance() over type()

Model a company’s staff. Create:

  • Employee base class with name, salary, and get_pay() method
  • FullTimeEmployee(Employee) — fixed monthly salary
  • PartTimeEmployee(Employee) — hourly rate × hours worked
  • Manager(Employee) — salary + sum of bonuses list
  • All should have meaningful __str__
  • A standalone function print_payroll(employees) that prints each employee’s pay
Expected output
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) — $7800
Solution
exercise_01.py
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)
test_exercise_01.py
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"
)

Create a set of mixin classes and combine them:

  • SerializeMixin — adds to_dict() and to_json() methods
  • ValidateMixin — adds validate() that checks all required fields are non-empty
  • TimestampMixin — adds created_at and updated_at, and an update() method that refreshes updated_at
  • ReprMixin — auto-generates __repr__ from instance __dict__
  • A User class that inherits all four mixins
Expected output
u = User(name="Alice", email="alice@example.com", age=30)
print(u.validate()) # True
print(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 timestamp
print(repr(u)) # User(name=Alice, email=new@x.com, age=30)
Solution