Skip to content
aviral gupta

// I2.3 · ~35 min · Intermediate

Inheritance and the MRO

After this lesson you can build a subclass that reuses and extends its base class, predict which method runs in a class hierarchy, and check types with isinstance and issubclass.

Lesson 3 of 6 in I2 Classes

You will be able to

  • Write a subclass that inherits, overrides and extends methods with super()
  • Predict which method runs by following the method resolution order, also with multiple inheritance
  • Check types with isinstance and issubclass, knowing that both include subclasses
  1. Warm-up · Activity 1 of 7

    Warm-up from module I1: you have written subclasses already. Every custom exception is one. What does this print?

    class AppError(Exception):
        pass
    
    
    err = AppError("disk full")
    print(isinstance(err, Exception), str(err))
  2. Predict · Activity 2 of 7

    Predict before you read on: introduce is defined only in Animal. What does this print?

    class Animal:
        def __init__(self, name):
            self.name = name
    
        def speak(self):
            return "..."
    
        def introduce(self):
            return f"{self.name} says {self.speak()}"
    
    
    class Dog(Animal):
        def speak(self):
            return "Woof"
    
    
    print(Dog("Rex").introduce())
  3. Practice · Activity 3 of 7

    Fill in the gap so that Manager lets Employee set up the name first.

    class Employee:
        def __init__(self, name):
            self.name = name
    
    
    class Manager(Employee):
        def __init__(self, name, reports):
            ____.__init__(name)
            self.reports = reports
    .__init__(name)
  4. Practice · Activity 4 of 7

    A diamond: B and C both derive from A. What does this print?

    class A:
        def hello(self):
            return "A"
    
    
    class B(A):
        pass
    
    
    class C(A):
        def hello(self):
            return "C"
    
    
    class D(B, C):
        pass
    
    
    print(D().hello(), [k.__name__ for k in D.__mro__])
  5. Practice · Activity 5 of 7

    Dog is a subclass of Animal, and rex = Dog(). Match each expression to its value.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. Every setup calls super(). What does this print?

    class Base:
        def setup(self):
            return ["Base"]
    
    
    class Logged(Base):
        def setup(self):
            return ["Logged"] + super().setup()
    
    
    class Cached(Base):
        def setup(self):
            return ["Cached"] + super().setup()
    
    
    class Service(Logged, Cached):
        def setup(self):
            return ["Service"] + super().setup()
    
    
    print(Service().setup())
  7. Apply · Activity 7 of 7

    Mini-task. Write a base class Shape with a name, an area() that returns 0.0, and describe(), which returns the name and the area to two decimals. Derive Circle(radius) and Square(side): each calls super().__init__ with its name and overrides area. Loop over a Circle(1) and a Square(3) and print describe() for each.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Notifiers with a shared send

send is written once, in Notifier, and calls self.format, which each subclass overrides. EmailNotifier extends __init__ with super(). SmsNotifier builds on the base format and shortens the text. LoudEmail inherits from two classes; its MRO puts Loud before EmailNotifier, so Loud's super().format reaches EmailNotifier, which adds the subject before Loud turns everything into capitals.

main.py

class Notifier:
    """Sends a message; subclasses decide how it looks."""

    def __init__(self, recipient: str) -> None:
        self.recipient = recipient

    def format(self, text: str) -> str:
        return text

    def send(self, text: str) -> str:
        return f"to {self.recipient}: {self.format(text)}"


class EmailNotifier(Notifier):
    def __init__(self, recipient: str, subject: str) -> None:
        super().__init__(recipient)
        self.subject = subject

    def format(self, text: str) -> str:
        return f"[{self.subject}] {text}"


class SmsNotifier(Notifier):
    LIMIT = 20

    def format(self, text: str) -> str:
        text = super().format(text)
        return text if len(text) <= self.LIMIT else text[: self.LIMIT - 3] + "..."


class Loud(Notifier):
    def format(self, text: str) -> str:
        return super().format(text).upper()


class LoudEmail(Loud, EmailNotifier):
    pass


notifiers: list[Notifier] = [
    EmailNotifier("ada@example.com", "Invoice"),
    SmsNotifier("0151-000000"),
    LoudEmail("ops@example.com", "Alert"),
]
for notifier in notifiers:
    print(notifier.send("Your order has shipped"))

print([cls.__name__ for cls in LoudEmail.__mro__])
print(isinstance(notifiers[2], EmailNotifier), issubclass(SmsNotifier, EmailNotifier))

Run it with

python main.py

Output

to ada@example.com: [Invoice] Your order has shipped
to 0151-000000: Your order has sh...
to ops@example.com: [ALERT] YOUR ORDER HAS SHIPPED
['LoudEmail', 'Loud', 'EmailNotifier', 'Notifier', 'object']
True False
  • SmsNotifier has no __init__ of its own, so it inherits Notifier's, which takes only the recipient.
  • LoudEmail("ops@example.com", "Alert") uses EmailNotifier's __init__, found by the MRO.
  • Loud derives from Notifier, yet its super().format call runs EmailNotifier.format.
  • notifiers is typed list[Notifier]: every subclass instance is a Notifier.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Books in the shop

Book is a Product with an author. Complete it: its __init__ takes name, price and author, lets Product store name and price with super(), and stores author. Books use a VAT rate of 0.07 instead of 0.19: override the class variable VAT. label() of a book adds the author in parentheses to Product's label, for example Dune: 10.70 EUR (Herbert). Reuse Product.label with super().

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Start Book.__init__ with super().__init__(name, price); then set self.author.

  2. Hint 2

    Put VAT = 0.07 in the body of Book. price_with_tax reads self.VAT, which now finds Book's value first.

  3. Hint 3

    def label(self) -> str: return f"{super().label()} ({self.author})"

Show a solution

One way to solve it. Yours can look different and still pass the checks.

class Product:
    VAT = 0.19

    def __init__(self, name: str, price: float) -> None:
        self.name = name
        self.price = price

    def price_with_tax(self) -> float:
        return round(self.price * (1 + self.VAT), 2)

    def label(self) -> str:
        return f"{self.name}: {self.price_with_tax():.2f} EUR"


class Book(Product):
    VAT = 0.07

    def __init__(self, name: str, price: float, author: str) -> None:
        super().__init__(name, price)
        self.author = author

    def label(self) -> str:
        return f"{super().label()} ({self.author})"
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

class Product:
    VAT = 0.19

    def __init__(self, name: str, price: float) -> None:
        self.name = name
        self.price = price

    def price_with_tax(self) -> float:
        return round(self.price * (1 + self.VAT), 2)

    def label(self) -> str:
        return f"{self.name}: {self.price_with_tax():.2f} EUR"


class Book(Product):
    def __init__(self, name: str, price: float, author: str) -> None:
        self.author = author

test_main.py

from main import Book, Product


def test_is_product():
    """A Book is a Product"""
    book = Book("Dune", 10.0, "Herbert")
    assert isinstance(book, Product), "Book must derive from Product"


def test_base_attributes():
    """Book keeps name and price through super().__init__"""
    book = Book("Dune", 10.0, "Herbert")
    got = getattr(book, "name", None), getattr(book, "price", None), getattr(book, "author", None)
    assert got == ("Dune", 10.0, "Herbert"), f"name, price and author are {got!r}"


def test_book_vat():
    """Books use 7% VAT"""
    got = Book("Dune", 10.0, "Herbert").price_with_tax()
    assert got == 10.7, f"price_with_tax() of a 10.00 book returned {got!r}, expected 10.7"


def test_product_unchanged():
    """Other products still use 19% VAT"""
    got = Product("Mug", 10.0).price_with_tax()
    assert got == 11.9, f"price_with_tax() of a 10.00 product returned {got!r}, expected 11.9"


def test_label():
    """A book's label ends with the author"""
    got = Book("Dune", 10.0, "Herbert").label()
    assert got == "Dune: 10.70 EUR (Herbert)", f"label() returned {got!r}"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 2 of 2

A job built from steps

Job(Validate, Save).run([]) should return ["validate", "save", "base"]: every class in the MRO adds its word once. First fix Save, which forgets to pass the log on with super(). Then write mro_names(cls), which returns the class names in cls.__mro__ without object, and count_steps(items), which counts the items that are instances of Step or any subclass of it.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    In Save.run, replace return log with return super().run(log), as Validate does.

  2. Hint 2

    cls.__mro__ is a tuple of classes; each has a __name__. Leave out the one that is object.

  3. Hint 3

    isinstance(item, Step) is True for Step and every subclass. sum(1 for item in items if ...) counts the matches.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

class Step:
    def run(self, log: list[str]) -> list[str]:
        log.append("base")
        return log


class Validate(Step):
    def run(self, log: list[str]) -> list[str]:
        log.append("validate")
        return super().run(log)


class Save(Step):
    def run(self, log: list[str]) -> list[str]:
        log.append("save")
        return super().run(log)


class Job(Validate, Save):
    pass


def mro_names(cls: type) -> list[str]:
    return [klass.__name__ for klass in cls.__mro__ if klass is not object]


def count_steps(items: list[object]) -> int:
    return sum(1 for item in items if isinstance(item, Step))
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

class Step:
    def run(self, log: list[str]) -> list[str]:
        log.append("base")
        return log


class Validate(Step):
    def run(self, log: list[str]) -> list[str]:
        log.append("validate")
        return super().run(log)


class Save(Step):
    def run(self, log: list[str]) -> list[str]:
        log.append("save")
        return log


class Job(Validate, Save):
    pass


def mro_names(cls: type) -> list[str]:
    return []


def count_steps(items: list[object]) -> int:
    return 0

test_main.py

from main import Job, Save, Step, Validate, count_steps, mro_names


def test_job_runs_every_step():
    """Job runs validate, save and base, once each"""
    got = Job().run([])
    assert got == ["validate", "save", "base"], f"Job().run([]) returned {got!r}"


def test_save_alone():
    """Save on its own also reaches the base step"""
    got = Save().run([])
    assert got == ["save", "base"], f"Save().run([]) returned {got!r}"


def test_mro_names():
    """mro_names lists the MRO without object"""
    got = mro_names(Job)
    assert got == ["Job", "Validate", "Save", "Step"], f"mro_names(Job) returned {got!r}"


def test_count_steps():
    """count_steps counts Step instances, subclasses included"""
    got = count_steps([Job(), Save(), Step(), "save", 3, Validate])
    assert got == 3, f"count_steps returned {got!r}, expected 3 (the class Validate itself is not an instance)"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common mistakes

Forgetting super().__init__()

class Animal:
    def __init__(self, name):
        self.name = name


class Dog(Animal):
    def __init__(self, name, breed):
        self.breed = breed


print(Dog("Rex", "collie").name)

What Python prints

AttributeError: 'Dog' object has no attribute 'name'

Why, and the fix

Dog's __init__ overrides Animal's, so Animal.__init__ never runs and self.name is never set. Python does not call the base __init__ for you. Start the subclass __init__ with super().__init__(name), then add what is new.

Passing self to super()

class Animal:
    def __init__(self, name):
        self.name = name


class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(self, name)
        self.breed = breed


Dog("Rex", "collie")

What Python prints

TypeError: Animal.__init__() takes 2 positional arguments but 3 were given

Why, and the fix

super() is already bound to the object, like self.method(), so the instance is passed for you. Passing self again adds one argument too many. Write super().__init__(name). Only the old form Animal.__init__(self, name), through the class, takes self explicitly.

Base classes in an impossible order

class A:
    pass


class B(A):
    pass


class C(A, B):
    pass

What Python prints

TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B

Why, and the fix

C(A, B) asks for A before B, but B is a subclass of A and must come before it. No order satisfies both, so the class statement fails. List subclasses before their bases, class C(B, A), or simply class C(B), since B already brings A.

Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Inherit, override, extend

class Dog(Animal): makes Dog a subclass of Animal. When an attribute or method is not found in Dog, Python looks in Animal, and on up the chain. A method with the same name in Dog overrides the one in Animal. To extend rather than replace it, call the base version with super(): super().__init__(name) lets Animal set up its part before Dog adds its own attributes. Leave that call out and the base class's attributes are never created. Methods are looked up on the object's class, so a base-class method that calls self.speak() runs the subclass's override.

The method resolution order

Every class has __mro__: the tuple of classes Python searches for an attribute, starting with the class itself and ending with object. With one base class it is a straight line. With several, class C(A, B), Python goes roughly depth-first and left to right, and visits a shared base class only once, after all its subclasses. super() does not mean "my parent": it means "the next class in the MRO of this object's class". That is why, when every method calls super(), each class in a diamond runs exactly once.

isinstance and issubclass

isinstance(obj, Animal) is True when obj is an Animal or an instance of any subclass of Animal. issubclass(Dog, Animal) asks the same about two classes, and a class counts as a subclass of itself. Both accept a tuple, as in isinstance(x, (int, float)). type(obj) is Animal is stricter: it is only True for the exact class, so it rejects a Dog. Prefer isinstance, so that subclasses keep working wherever the base class does. Built-in types have a hierarchy too: bool is a subclass of int.

Sources

Last reviewed September 29, 2026