Inheritance and Method Resolution Order
Objectives
By the end of this chapter, you should be able to:
- Explain the relationship between inheritance and code duplication
- Use
super()to call a parent class’s methods - Explain method resolution order (MRO) in Python 3
💡 Why this matters: Without inheritance, related classes end up copy-pasting the same
__init__and methods. Inheritance lets a class reuse and extend another class’s behavior instead of duplicating it.
Single Inheritance
Say you have a general Vehicle class:
class Vehicle:
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
def honk(self):
return "Beep!"
A Car is a more specific kind of vehicle. Instead of rewriting __init__, it can inherit from Vehicle and call super() to reuse the parent’s setup:
class Car(Vehicle):
def __init__(self, make, model, year):
super().__init__(make, model, year)
self.wheels = 4
super().__init__(...) calls Vehicle’s __init__ on this instance, so Car doesn’t need to repeat the assignment logic. Car also gets .honk() for free, since it inherits everything Vehicle defines:
mack_truck = Vehicle("Mack", "Titan", 2015)
car = Car("Honda", "Civic", 2004)
mack_truck.honk() # "Beep!"
car.honk() # "Beep!"
car.wheels # 4
Multiple Inheritance and MRO
Python also allows a class to inherit from more than one parent at once. Consider two unrelated classes:
class Aquatic:
def __init__(self, name):
self.name = name
def swim(self):
return f"{self.name} is swimming"
def greet(self):
return f"I am {self.name} of the sea!"
class Ambulatory:
def __init__(self, name):
self.name = name
def walk(self):
return f"{self.name} is walking"
def greet(self):
return f"I am {self.name} of the land!"
A penguin can swim and walk, so it makes sense to inherit from both:
class Penguin(Aquatic, Ambulatory):
def __init__(self, name):
super().__init__(name=name)
jaws = Aquatic("Jaws")
lassie = Ambulatory("Lassie")
waddles = Penguin("Waddles")
jaws.swim() # "Jaws is swimming"
jaws.walk() # AttributeError, Aquatic has no walk method
jaws.greet() # "I am Jaws of the sea!"
lassie.swim() # AttributeError, Ambulatory has no swim method
lassie.walk() # "Lassie is walking"
lassie.greet() # "I am Lassie of the land!"
waddles.swim() # "Waddles is swimming"
waddles.walk() # "Waddles is walking"
waddles.greet() # "I am Waddles of the sea!"
Penguin inherits both .swim() and .walk() without redefining either. But both parents define .greet(), so which one wins? Waddles.greet() resolves to "of the sea!", from Aquatic, because Aquatic was listed first in class Penguin(Aquatic, Ambulatory).
This lookup order is called the method resolution order (MRO): the sequence Python searches through, left to right, to find a method or attribute. You can inspect it directly:
Penguin.__mro__
# (<class 'Penguin'>, <class 'Aquatic'>, <class 'Ambulatory'>, <class 'object'>)
Penguin.mro()
# same information, as a list
help(Penguin)
# shows the full MRO along with inherited methods
Every class’s MRO ends with object, the base class that all Python classes ultimately inherit from.
Custom Exception Classes
Back in the debugging module, you raised built-in errors like ValueError with raise. Now that you know how inheritance works, you can define your own error types the same way you’d define any other class: by inheriting from Exception:
class InsufficientFundsError(Exception):
pass
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(f"Cannot withdraw {amount}, balance is only {self.balance}")
self.balance -= amount
account = BankAccount(50)
account.withdraw(100)
# InsufficientFundsError: Cannot withdraw 100, balance is only 50
Because InsufficientFundsError inherits from Exception, it works everywhere a built-in exception would. You can raise it, except it specifically, and it comes with the same str() behavior showing your message:
try:
account.withdraw(100)
except InsufficientFundsError as e:
print(f"Transaction failed: {e}")
A custom exception with just pass in its body is often all you need. Its real value is in its name, letting except statements elsewhere in your code target this exact situation instead of catching a generic ValueError that might mean something else entirely.
Try It
- Write two small classes with no shared parent, each defining a differently-named method, then write a third class that inherits from both.
- Give both parent classes a method with the same name, and predict (then verify) which one a child instance resolves to.
- Call
.mro()on your child class and read the order out loud. - Define your own exception class inheriting from
Exception,raiseit from a function under some condition, and catch it specifically withexcept YourError:.
Recap
- Inheritance lets a class reuse a parent’s methods instead of duplicating them;
super()calls the parent’s version of a method (commonly__init__). - A class can inherit from multiple parents at once.
- When multiple parents define the same method name, Python resolves it using the method resolution order (MRO): left to right, in the order parents were listed.
ClassName.__mro__orClassName.mro()shows the exact lookup order, which always ends withobject.- Custom exceptions are just classes that inherit from
Exception. Defining one lets youraiseandexcepta specific, named error instead of a generic built-in one.
Next lesson: special methods and polymorphism.