CodingNic

Object Oriented Programming

Inheritance and Method Resolution Order

Object Oriented Programming 25 min read

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:

python
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:

python
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:

python
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:

python
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:

python
class Penguin(Aquatic, Ambulatory):
    def __init__(self, name):
        super().__init__(name=name)
python
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:

python
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:

python
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
python
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:

python
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

  1. Write two small classes with no shared parent, each defining a differently-named method, then write a third class that inherits from both.
  2. Give both parent classes a method with the same name, and predict (then verify) which one a child instance resolves to.
  3. Call .mro() on your child class and read the order out loud.
  4. Define your own exception class inheriting from Exception, raise it from a function under some condition, and catch it specifically with except 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__ or ClassName.mro() shows the exact lookup order, which always ends with object.
  • Custom exceptions are just classes that inherit from Exception. Defining one lets you raise and except a specific, named error instead of a generic built-in one.

Next lesson: special methods and polymorphism.