Skip to content

Inheritance and super

~20 min ยท prerequisite: Encapsulation and properties

What you will be able to do

  • Create a subclass that reuses the attributes and methods of a parent class.
  • Replace a parent's method with your own version, and call the parent's version with super().
  • Read a class's method resolution order with __mro__ and use isinstance to check a parent relationship.

The idea

Inheritance lets you build a new class from an existing one. The new class is called the subclass, and the existing class is the parent class or superclass. The subclass gets every attribute and method of the parent without copying any code. You write only what is different.

Take a bank. A savings account is a basic account that also earns interest. Instead of rewriting the deposit and description code, the savings account inherits it and adds the interest. You write the subclass by putting the parent's name in parentheses, as in class SavingsAccount(BankAccount):.

A subclass can override a method. Overriding means defining a method with the same name, so the subclass version runs when you call it. The parent's version is still there. To reach it, call super(). The expression super() gives you the parent part of the current object, so super().describe() runs the parent's describe. The most common use is in __init__. Calling super().__init__(owner) lets the parent set up its attributes, so the setup code stays in one place.

Python searches for a method in the object's class first, then in its parents. The search order is stored in __mro__, short for method resolution order. The last entry is object, the base of every class.

Use isinstance(savings, BankAccount) to check whether an object belongs to a class or any of its subclasses. The answer is True here, because a savings account is also a bank account.

Inheritance is useful when the subclass really is a kind of the parent. If you only want to reuse a few lines, a plain function or a separate class is often clearer.

Worked example

class BankAccount:
    def __init__(self, owner):
        self.owner = owner
        self._balance = 0

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("deposit must be positive")
        self._balance += amount

    def describe(self):
        return f"{self.owner}: {self._balance}"


class SavingsAccount(BankAccount):
    def __init__(self, owner, rate):
        super().__init__(owner)
        self.rate = rate

    def add_interest(self):
        self._balance += self._balance * self.rate

    def describe(self):
        return f"Savings for {super().describe()} at {self.rate}"


savings = SavingsAccount("Ada", 0.05)
savings.deposit(100)
savings.add_interest()
print(savings.describe())

The output is Savings for Ada: 105.0 at 0.05. Line by line, this is what Python does:

  1. class SavingsAccount(BankAccount): creates a subclass. Every method of BankAccount now exists on SavingsAccount objects too.
  2. super().__init__(owner) calls the parent's __init__ with owner. That sets owner and _balance. The savings account then sets its own rate.
  3. add_interest reads _balance, which the parent's __init__ created, and adds 5% of it.
  4. describe overrides the parent's version. It calls super().describe() to get "Ada: 105.0" and wraps that text.
  5. savings.deposit(100) uses the inherited method, and print shows the overridden describe.

Try it

The first block checks the inherited and overridden methods. The second block builds a shape hierarchy and checks isinstance and __mro__.

class BankAccount:
    def __init__(self, owner):
        self.owner = owner
        self._balance = 0

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

    def describe(self):
        return f"{self.owner}: {self._balance}"


class SavingsAccount(BankAccount):
    def __init__(self, owner, rate):
        super().__init__(owner)
        self.rate = rate

    def describe(self):
        return f"Savings for {super().describe()}"


savings = SavingsAccount("Ada", 0.05)
savings.deposit(100)
assert savings.describe() == "Savings for Ada: 100"
assert isinstance(savings, BankAccount)
assert not isinstance(BankAccount("Bo"), SavingsAccount)
assert SavingsAccount.__mro__ == (SavingsAccount, BankAccount, object)
class Shape:
    def __init__(self, name):
        self.name = name

    def describe(self):
        return self.name


class Square(Shape):
    def __init__(self, side):
        super().__init__("square")
        self.side = side

    def area(self):
        return self.side * self.side


sq = Square(4)
assert sq.describe() == "square"
assert sq.area() == 16
assert sq.name == "square"
assert isinstance(sq, Shape)

Common mistakes

  1. Leaving out super().__init__. If SavingsAccount.__init__ only sets rate, the parent never runs. Calling deposit then gives:

    AttributeError: 'SavingsAccount' object has no attribute '_balance'
    

    Call super().__init__(owner) first in the subclass's __init__.

  2. Passing self to super(). The call super().__init__(self, owner) adds an extra argument, because super() already supplies self. It gives:

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

    Write super().__init__(owner).

  3. Naming a parent class that does not exist yet. Writing class Child(Parent): above the definition of Parent gives:

    NameError: name 'Parent' is not defined
    

    Define the parent class first.

  4. Calling a subclass method on a parent object. BankAccount("Bo").add_interest() gives:

    AttributeError: 'BankAccount' object has no attribute 'add_interest'
    

    Only a SavingsAccount has add_interest. Check the object's class with type.

Practice

  1. Using the Shape class from the second block, create a subclass Rectangle with width and height. Give it an area method, and call super().__init__("rectangle") in its __init__. Assert that Rectangle(2, 3).area() is 6.
  2. Create a subclass LoudShape of Shape whose describe returns the parent's text in uppercase, using super().describe(). Assert that LoudShape("circle").describe() is "CIRCLE".
  3. Create a class Animal with a method speak that returns "...". Create Cat(Animal) whose speak returns "Meow". Assert that Cat().speak() is "Meow" and that isinstance(Cat(), Animal) is True.
  4. Create a class Vehicle whose __init__ takes no arguments and sets wheels to 2. Create Car(Vehicle) whose __init__ calls super().__init__() and then sets wheels to 4. Assert that Car().wheels is 4.
  5. Stretch: create Vehicle with __init__(self, name) that stores name. Create Car(Vehicle) with __init__(self, name, wheels), which calls super().__init__(name) and stores wheels. Create ElectricCar(Car) with __init__(self, name, battery_kwh), which calls super().__init__(name, 4) and stores battery_kwh. Assert that ElectricCar("Volt", 60).name is "Volt", that its wheels is 4, and that ElectricCar.__mro__ starts with ElectricCar, Car, Vehicle.
Solution
  1. ```python class Shape: def init(self, name): self.name = name

    def describe(self): return self.name

class Rectangle(Shape): def init(self, width, height): super().init("rectangle") self.width = width self.height = height

   def area(self):
       return self.width * self.height

assert Rectangle(2, 3).area() == 6 ```

  1. ```python class Shape: def init(self, name): self.name = name

    def describe(self): return self.name

class LoudShape(Shape): def describe(self): return super().describe().upper()

assert LoudShape("circle").describe() == "CIRCLE" ```

  1. ```python class Animal: def speak(self): return "..."

class Cat(Animal): def speak(self): return "Meow"

assert Cat().speak() == "Meow" assert isinstance(Cat(), Animal) ```

  1. ```python class Vehicle: def init(self): self.wheels = 2

class Car(Vehicle): def init(self): super().init() self.wheels = 4

assert Car().wheels == 4 ```

  1. ```python class Vehicle: def init(self, name): self.name = name

class Car(Vehicle): def init(self, name, wheels): super().init(name) self.wheels = wheels

class ElectricCar(Car): def init(self, name, battery_kwh): super().init(name, 4) self.battery_kwh = battery_kwh

volt = ElectricCar("Volt", 60) assert volt.name == "Volt" assert volt.wheels == 4 assert ElectricCar.mro[:3] == (ElectricCar, Car, Vehicle) ```

Recap

  • A subclass inherits the attributes and methods of its parent, and can override a method.
  • super() gives access to the parent's version of a method, such as super().__init__(...).
  • isinstance checks parent relationships, and __mro__ shows the order in which Python searches classes.

Sources