Beginner · Lesson 38 of 94

Inheritance and super

Watch this lesson

View full playlist ↗

Read the lesson

What you'll learn

Concept

Inheritance lets one class be built on another. class SavingsAccount(Account): says a savings account is an account: it gets every attribute and method of Account without repeating a line, and it can add new ones or change existing ones. Account is the parent (or base, or superclass); SavingsAccount is the child (or subclass). An instance of the child is also an instance of the parent, so isinstance(savings, Account) is True and any code written for accounts accepts it.

Inheritance and super: Account; owner, balance; deposit(), withdraw() → SavingsAccount; + rate, add_interest(); withdraw() overridden → CheckingAccount; + overdraft
Inheritance and super · Figure 1 · Open for full size

Overriding means defining a method in the child with the same name as one in the parent. When called on a child instance, the child's version wins. Often you want to extend rather than replace: do what the parent does, plus something. super().withdraw(amount) calls the parent's version from inside the override, so the parent's checks still run and the child adds its own behavior around them. The same applies to __init__: a child's initializer nearly always starts with super().__init__(...) to set up the inherited attributes, then adds its own.

Situation Write
inherit everything, add nothing class Child(Parent): pass
add a new method define it in the child
replace a parent method entirely define the same name in the child
extend a parent method define it, and call super().method(...) inside
add attributes in the child super().__init__(...) first, then self.new = ...

When Python looks up a method, it searches the instance's class, then its parent, then the parent's parent, up to object, the root of every class. That search order is the method resolution order, and SavingsAccount.__mro__ shows it. With single inheritance it is the chain of parents; with multiple parents Python computes a consistent order, which you will mostly meet in mixins in other people's code. The MRO explains every "which version runs?" question.

Inheritance and super: savings.withdraw(50) → run it; super() continues the search → run Account.withdraw → object → AttributeError
Inheritance and super · Figure 2 · Open for full size

The question to ask before inheriting is is a: is a savings account an account, in every way that matters, such that any code expecting an account would be happy with a savings account? If yes, inherit. If the relationship is really has a or uses a, such as a car and its engine, the right tool is composition, covered in the last lesson of this module. Inheritance is powerful and easy to overuse; a hierarchy more than two or three levels deep is almost always a sign that composition would have been clearer.

Code

A child that inherits, adds a method, and overrides another by extending it with super(). The parent's guard still runs inside the child's version.

# savings.py
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("insufficient funds")
        self.balance -= amount

    def __repr__(self):
        return f"{type(self).__name__}({self.owner!r}, {self.balance})"


class SavingsAccount(Account):
    def __init__(self, owner, balance=0, rate=0.02):
        super().__init__(owner, balance)
        self.rate = rate

    def add_interest(self):
        self.deposit(round(self.balance * self.rate, 2))

    def withdraw(self, amount):
        if amount > 500:
            raise ValueError("savings withdrawals are capped at 500")
        super().withdraw(amount)


s = SavingsAccount("ada", 1000, rate=0.05)
s.add_interest()
print(s)
s.withdraw(100)
print(s)
for attempt in (600, 5000):
    try:
        s.withdraw(attempt)
    except ValueError as err:
        print("refused:", err)
print(isinstance(s, Account), isinstance(s, SavingsAccount))
Output
SavingsAccount('ada', 1050.0)
SavingsAccount('ada', 950.0)
refused: savings withdrawals are capped at 500
refused: savings withdrawals are capped at 500
True True

The method resolution order, printed, and a lookup that walks it. describe is found on the grandparent because neither child defines it.

# mro.py
class Animal:
    def describe(self):
        return f"{type(self).__name__} says {self.sound()}"

    def sound(self):
        return "..."


class Dog(Animal):
    def sound(self):
        return "woof"


class Puppy(Dog):
    pass


print([cls.__name__ for cls in Puppy.__mro__])
print(Puppy().describe())
print(Animal().describe())
Output
['Puppy', 'Dog', 'Animal', 'object']
Puppy says woof
Animal says ...

Forgetting super().__init__ is the classic inheritance bug: the child sets its own attribute and never sets up the parent's, so the inherited method fails.

# forgot_super.py
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance


class Broken(Account):
    def __init__(self, owner, rate):
        self.rate = rate


class Fixed(Account):
    def __init__(self, owner, rate):
        super().__init__(owner)
        self.rate = rate


print(Fixed("ada", 0.02).balance)
print(Broken("ada", 0.02).balance)
Output
0
Traceback (most recent call last):
  File "forgot_super.py", line 19, in <module>
    print(Broken("ada", 0.02).balance)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'Broken' object has no attribute 'balance'

Code written for the parent works with every child. The function knows nothing about savings or checking accounts, and does not need to.

# substitutable.py
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance


class SavingsAccount(Account):
    kind = "savings"


class CheckingAccount(Account):
    kind = "checking"


def total_balance(accounts):
    return sum(a.balance for a in accounts)


accounts = [SavingsAccount("ada", 300), CheckingAccount("linus", 120), Account("grace", 80)]
print(total_balance(accounts))
print([getattr(a, "kind", "plain") for a in accounts])
Output
500
['savings', 'checking', 'plain']

Try it yourself

Create vehicles.py with a Vehicle class holding make, model, and speed with accelerate(amount) and a __repr__. Add Car(Vehicle) with a doors attribute set through super().__init__, and Bicycle(Vehicle) that overrides accelerate to cap speed at 40 by calling super() then clamping. Print the MRO of Car, then put one of each in a list and accelerate them all in one loop.

Common mistakes

What's next

Children can stand in for their parents. Next you will see the pillar that makes that useful: polymorphism, and Python's relaxed version of it, duck typing.

Course overview