Inheritance 2

Introduction to Software Engineering (CSSE 1001)

Author

Paul Vrbik

Published

May 21, 2026

Abstract Base Class

It is common to have an abstract base class that doesn’t have concrete methods/attributes but enforces contracts in children classes.

You are not meant to instantiate objects from these abstract classes directly.

Example: an abstract Shape class that has an area() method, without a concrete implementation. Every (concrete) child class of Shape, must provide a concrete implementation of the area() method.

Abstract base classes can sometimes have concrete implementations for some methods (especially if those are meant to be used as is by the child classes).

Example Abstract Base Class

import math


class Shape:
    def area(self) -> float:
        raise NotImplementedError('Subclasses must implement area()')

    def perimeter(self) -> float:
        raise NotImplementedError('Subclasses must implement perimeter()')


class Circle(Shape):
    def __init__(self, radius: float) -> None:
        self._radius = radius

    def area(self) -> float:
        return math.pi * self._radius * self._radius

    def perimeter(self) -> float:
        return 2 * math.pi * self._radius


class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        self._width = width
        self._height = height

    def area(self) -> float:
        return self._width * self._height

    def perimeter(self) -> float:
        return 2 * (self._width + self._height)


shapes = [
    Circle(3.0),
    Rectangle(2.5, 3.0),
    Rectangle(1.0, 1.0)
]

for shape in shapes:
    # Useful way to get the name of the class an object belongs to
    shape_name = type(shape).__name__

    # We don't have to check what specific kind of shape we have,
    # since all shapes should have area() and perimeter() which perform
    # the appropriate behaviour for the type of shape they are
    area = shape.area()
    perimeter = shape.perimeter()

    print(f'{shape_name} has area {area} and perimeter {perimeter}')
Circle has area 28.274333882308138 and perimeter 18.84955592153876
Rectangle has area 7.5 and perimeter 11.0
Rectangle has area 1.0 and perimeter 4.0
Note

Optionally, you can look into using the ABC class and abstractmethod decorator from the abc module in your own projects. For this course, however, you must not do this in your assignments.

Inheritance models

Python supports several inheritance models:

  • Single inheritance — a class inherits from a single parent class
  • Multilevel inheritance — a class inherits from a child class, which in turn inherits from another parent class. It forms a linear chain of inheritance, creating a parent -> child -> grandchild relationship.
  • Hierarchical inheritance — multiple child classes inherit from a single parent class
  • Multiple inheritance — a class inherits from multiple parent classes. This is an advanced technique.

Method Resolution Order (MRO)

Question: If a class inherits from two parents, and both parents have a method with the same name, which one does Python use?

  • MRO stands for Method Resolution Order — it defines the order in which Python looks through classes to find a method or attribute when it’s called on an object.
  • It determines which method gets called when there are multiple implementations.
  • Stored in cls.__mro__

Single Inheritance

Consider the following code snippet (from the course notes). Note object is the universal class.

class A(object):
    def __init__(self, x):
        self.x = x
    def f(self):
        return self.x
    def g(self):
        return 2 * self.x
    def fg(self):
        return self.f() - self.g()

>>> a = A(3)
>>> a.x
3
>>> a.f()
6
>>> a.fg()
-3

Note: even though the B class does have a grandparent (the object class), we still say it’s single inheritance, since every class in Python inherits from object eventually.

class B(A):
    # __init__, f, and fg are all inherited from the A class

    # override the g method from A
    def g(self):
        return self.x ** 2

>>> b = B(7)
>>> b.x
7
>>> b.f()
7
>>> b.g()
49
>>> b.fg()
-42

Multilevel Inheritance

class C(B):
    # Extend the __init__ inherited from A via B
    def __init__(self, x, y):
        super().__init__(x)
        self.y = y
    
    # Inherit the f method from A via B

    # Inherit the g method from B

    # Extend the fg method inherited from A via B
    def fg(self):
        return super().fg() * self.y

>>> c = C(3, 5)
>>> c.x
3
>>> c.y
5
>>> c.f()
3
>>> c.g()
9
>>> c.fg()
-30

Hierarchical Inheritance

class D(A):
    # Inherit __init__, g, and fg from A

    # Override the f method
    def f(self):
        return -2 * self.g()

>>> d = D(3)
>>> d.x
3
>>> d.f()
-12
>>> d.g()
6
>>> d.fg()
-18

The UML diagram for these classes is shown below:

Multiple Inheritance

class E(B, D):
    pass
  • E inherits from both B and D.
  • B and D both inherit from A.
  • If B and D provide different implementations of a method that E inherits, which one should it use?
  • Python resolves this with the MRO C3 linearisation.

C3 Linearisation

  • Child classes are checked before parents
  • Parents are checked in the order they are listed in the class definition
  • If a class appears multiple times in the MRO, only the last occurrence is kept
>>> E.mro()
[<class '__main__.E'>, <class '__main__.B'>,
 <class '__main__.D'>, <class '__main__.A'>, <class 'object'>]

>>> for cls in E.__mro__:
...     print(cls.__name__)
E
B
D
A
object

>>> e = E(3)
>>> e.x
3
>>> e.f()
-18
>>> e.g()
9
>>> e.fg()
-27

super() function and the MRO

The super() function follows the Method Resolution Order, not just the immediate parent.

class A:
    def ping(self):
        print("A")

class B(A):
    def ping(self):
        print("B")
        super().ping()

class C(A):
    def ping(self):
        print("C")
        super().ping()

class D(B, C):
    def ping(self):
        print("D")
        super().ping()

>>> D().ping()
D
B
C
A

MRO in a more complex lattice

class A: pass
class B: pass
class C(A): pass
class D(A, B): pass
class E(C, D, B): pass

print([cls.__name__ for cls in E.__mro__])
# ['E', 'C', 'D', 'A', 'B', 'object']

Exercise

Implement some classes that inherit from the Shape class and support the given interface.

class Shape:
    pass  # Implement Shape

class Rectangle(Shape):
    pass  # Implement Rectangle

class Square(Rectangle):
    pass  # Implement Square

r = Rectangle(10, 5)
s = Square(10)
r < s   # True

Summary

When a class inherits from multiple parents, it’s possible for more than one parent to define the same method or attribute.

To avoid confusion and ensure consistency, Python uses Method Resolution Order (MRO) to determine the order in which classes are searched.

MRO follows a well-defined path based on class hierarchy and inheritance order, ensuring that each method or attribute is found in a predictable and logical way — especially important in complex cases such as the diamond pattern.