OOP II: Inheritance and Method Resolution

When your dad is an object

Author

Karsten Naert

Published

November 15, 2025

Prerequisites

Before diving into this lecture, you should be comfortable with these concepts:

  • Object / class / instance terminology
  • Methods and __init__
  • Class variables
  • Basic class syntax: class MyClass: ...

Let’s also recall the relationship: “The object my_object is an instance of the class MyClass

Basic Inheritance

Extending functionality

Sometimes you want to create multiple classes that behave similarly but with slight differences. Think about file streams: sys.stdout and file objects both have:

  • An open() method
  • A close() method
  • A write() method

Rather than duplicating code, we can use inheritance. You create one base class with shared functionality and multiple derived classes (or subclasses) with specific behavior.

class OriginalClass:
    ...  # This is the base class

class NewClass(OriginalClass):
    ...  # This is the derived class

Here’s a simple example:

class TwoNumbers:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def show_numbers(self):
        print(f'TwoNumbers object with a={self.a} and b={self.b}')
two_numbers = TwoNumbers(3, 4)
two_numbers.show_numbers()
TwoNumbers object with a=3 and b=4

Now let’s create specialized versions:

class SumTwoNumbers(TwoNumbers):
    def result(self):
        return self.a + self.b

class DifferenceTwoNumbers(TwoNumbers):
    def result(self):
        return self.a - self.b

These new classes will inherit all the functionality from the parent class TwoNumber, but in addition the method result method is a new method that did not exist in the base class. We use these classes like this:

sum_obj = SumTwoNumbers(3, 4)
sum_obj.show_numbers()  # Inherited method
print("Sum:", sum_obj.result())  # New method

diff_obj = DifferenceTwoNumbers(3, 4)  
diff_obj.show_numbers()  # Inherited method
print("Difference:", diff_obj.result())  # New method
TwoNumbers object with a=3 and b=4
Sum: 7
TwoNumbers object with a=3 and b=4
Difference: -1

Terminology

  • Base class (or parent class, superclass): The original class (TwoNumbers)
  • Derived class (or child class, subclass): The inheriting class (SumTwoNumbers)
  • Sibling classes: Classes with the same parent (SumTwoNumbers and DifferenceTwoNumbers)
Exercise
  1. Create a ProductTwoNumbers class that multiplies the two numbers.

  2. Create a base class TwoLists that stores two lists and provides .append1(x) and .append2(x) methods. Then create:

    • ShortestTwoLists: has an append(x) method that adds to the shorter list
    • LongestTwoLists: has an append(x) method that adds to the longer list

Testing relationships

issubclass and isinstance

Use issubclass() to test inheritance relationships:

print("DifferenceTwoNumbers is subclass of itself:", 
      issubclass(DifferenceTwoNumbers, DifferenceTwoNumbers))
print("DifferenceTwoNumbers is subclass of TwoNumbers:", 
      issubclass(DifferenceTwoNumbers, TwoNumbers))
print("TwoNumbers is subclass of DifferenceTwoNumbers:", 
      issubclass(TwoNumbers, DifferenceTwoNumbers))
DifferenceTwoNumbers is subclass of itself: True
DifferenceTwoNumbers is subclass of TwoNumbers: True
TwoNumbers is subclass of DifferenceTwoNumbers: False

Use isinstance() to test if an object belongs to a class:

sum_obj = SumTwoNumbers(1, 2)
print("sum_obj is instance of SumTwoNumbers:", isinstance(sum_obj, SumTwoNumbers))
print("sum_obj is instance of TwoNumbers:", isinstance(sum_obj, TwoNumbers))
print("sum_obj is instance of DifferenceTwoNumbers:", isinstance(sum_obj, DifferenceTwoNumbers))
sum_obj is instance of SumTwoNumbers: True
sum_obj is instance of TwoNumbers: True
sum_obj is instance of DifferenceTwoNumbers: False

The object hierarchy

In Python, every class inherits from object by default:

print("TwoNumbers inherits from object:", issubclass(TwoNumbers, object))
print("int inherits from object:", issubclass(int, object))
print("Everything is an object:", isinstance(42, object), isinstance(sum_obj, object))
TwoNumbers inherits from object: True
int inherits from object: True
Everything is an object: True True

These are equivalent:

class MyClass:
    pass

class MyClass(object):  # Explicit, but unnecessary
    pass
Tip

In modern Python, it is never necessary to inherit from object, so don’t do it unless you have a very good reason to.

It is related to the distinction between “old style” and “new style” classes which existed in Python 2.x.

Method overriding

You can override (replace) methods from the parent class:

class StringContainer:
    def __init__(self, text1, text2):
        self.text1 = text1
        self.text2 = text2

    def format(self):
        return f'StringContainer containing "{self.text1}" and "{self.text2}"'
container = StringContainer("Alice", "Bob")
print(container.format())
StringContainer containing "Alice" and "Bob"
class SingleQuotedStringContainer(StringContainer):
    def format(self):  # Override the parent's format method
        return f"StringContainer containing '{self.text1}' and '{self.text2}'"
quoted_container = SingleQuotedStringContainer("Alice", "Bob")
print(quoted_container.format())
StringContainer containing 'Alice' and 'Bob'

You can override any method, including __init__, properties, class methods, and static methods:

class Operation:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    @property
    def result(self):
        return self.a + self.b

class MultiplyOperation(Operation):
    @property  
    def result(self):  # Override the property
        return self.a * self.b
add_op = Operation(3, 4)
mult_op = MultiplyOperation(3, 4)
print(f"Addition: {add_op.result}")
print(f"Multiplication: {mult_op.result}")
Addition: 7
Multiplication: 12
Exercise
  1. Create an AngularStringContainer that formats text with <> brackets instead of quotes.

  2. Create a TripleStringContainer that:

    • Inherits from StringContainer
    • Overrides __init__ to accept three texts
    • Uses the original format() method (what happens?)
  3. Create a class VerboseDict inheriting from dict that prints messages when items are accessed or modified:

    verbose_dict = VerboseDict(a=1, b=2)
    print(verbose_dict['a'])  # Should print "Requesting a" then 1
    verbose_dict['b'] = 10    # Should print "Setting b to 10"

Using parent methods with super()

Sometimes you want to extend rather than completely replace a parent’s method:

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

    def say_hello(self):
        print(f'Hi {self.name}')

class Friend(Person):
    def say_hello(self):
        print(f'Hello {self.name}, my good friend!')

You can explicitly call the parent’s method:

friend = Friend('Alice')
friend.say_hello()  # Child method
Person.say_hello(friend)  # Parent method explicitly
Hello Alice, my good friend!
Hi Alice

But super() is cleaner and more flexible:

class Friend(Person):
    def say_hello(self):
        super().say_hello()  # Call parent method
        print('Welcome, my good friend!')
friend = Friend('Alice')
friend.say_hello()
Hi Alice
Welcome, my good friend!

This pattern is especially common with __init__:

class PersonWithAge(Person):
    def __init__(self, name, age):
        super().__init__(name)  # Initialize parent
        self.age = age          # Add our own initialization

    def describe(self):
        print(f'A {self.age}-year-old person named {self.name}')
person_with_age = PersonWithAge('Bob', 25)
person_with_age.say_hello()
person_with_age.describe()
Hi Bob
A 25-year-old person named Bob
Warning

Order matters: usually you want to call super().__init__() first in your child’s __init__ method, then add your own initialization code.

Multiple inheritance and MRO

Multiple parents

A class can inherit from multiple classes simultaneously. Each class can deliver some functionality (methods, properties, …) that end up in the subclass.

class LineFormatter:
    def __init__(self):
        self.separator = ' | '

    def identify(self):
        print('Hi, I am LineFormatter')
        
    def format_line(self, items):
        return self.separator.join(items)

class ByteDecoder:
    def __init__(self):
        self.encoding = 'utf-8'

    def identify(self):
        print('Hi, I am ByteDecoder')

    def decode_bytes(self, byte_list):
        return [b.decode(self.encoding) for b in byte_list]
class ByteFormatter(LineFormatter, ByteDecoder):  # Multiple inheritance
    def __init__(self):
        self.encoding = 'utf-8'
        self.separator = ' | '

    def identify(self):
        print('Hi, I am ByteFormatter')

    def format_bytes(self, byte_list):
        decoded = self.decode_bytes(byte_list)  # From ByteDecoder
        formatted = self.format_line(decoded)   # From LineFormatter
        return formatted
formatter = ByteFormatter()
formatter.identify()
result = formatter.format_bytes([b'\xce\xb1', b'\xce\xb2', b'\xce\xb3'])
print(result)
Hi, I am ByteFormatter
α | β | γ

Method Resolution Order (MRO)

With multiple inheritance, which parent’s method gets called? Python uses the Method Resolution Order:

print("ByteFormatter MRO:")
for i, cls in enumerate(ByteFormatter.__mro__):
    print(f"  {i}: {cls.__name__}")
ByteFormatter MRO:
  0: ByteFormatter
  1: LineFormatter
  2: ByteDecoder
  3: object

When you call super(), Python looks for the next class in the MRO, not necessarily the immediate parent:

class Identifiable:
    def identify(self):
        print('Hi, I am Identifiable')

class LineFormatter(Identifiable):
    def __init__(self):
        super().__init__()
        self.separator = ' | '

    def identify(self):
        print('Hi, I am LineFormatter')
        super().identify()  # Continue the chain
        
    def format_line(self, items):
        return self.separator.join(items)

class ByteDecoder(Identifiable):
    def __init__(self):
        super().__init__()
        self.encoding = 'utf-8'

    def identify(self):
        print('Hi, I am ByteDecoder')
        super().identify()  # Continue the chain

    def decode_bytes(self, byte_list):
        return [b.decode(self.encoding) for b in byte_list]

class ByteFormatter(LineFormatter, ByteDecoder):
    def __init__(self):
        super().__init__()

    def identify(self):
        print('Hi, I am ByteFormatter')
        super().identify()  # Start the chain

    def format_bytes(self, byte_list):
        decoded = self.decode_bytes(byte_list)
        formatted = self.format_line(decoded)
        return formatted
print("New MRO:")
for i, cls in enumerate(ByteFormatter.__mro__):
    print(f"  {i}: {cls.__name__}")

print("\nCalling identify():")
formatter = ByteFormatter()
formatter.identify()
New MRO:
  0: ByteFormatter
  1: LineFormatter
  2: ByteDecoder
  3: Identifiable
  4: object

Calling identify():
Hi, I am ByteFormatter
Hi, I am LineFormatter
Hi, I am ByteDecoder
Hi, I am Identifiable

Handling __init__ with multiple inheritance

When using super().__init__() with multiple inheritance, be careful. Sometimes you need to call parent constructors explicitly:

class LineFormatter:
    def __init__(self, separator=' | '):
        self.separator = separator

class ByteDecoder:
    def __init__(self, encoding='utf-8'):
        self.encoding = encoding

class ByteFormatter(LineFormatter, ByteDecoder):
    def __init__(self, separator=' | ', encoding='utf-8'):
        # Explicitly initialize both parents
        LineFormatter.__init__(self, separator)
        ByteDecoder.__init__(self, encoding)
formatter = ByteFormatter(separator=' <-> ', encoding='latin1')
print(f"Separator: '{formatter.separator}'")
print(f"Encoding: '{formatter.encoding}'")
Separator: ' <-> '
Encoding: 'latin1'
Tip

Multiple inheritance can be tricky. When in doubt:

  1. Keep inheritance hierarchies simple
  2. Use composition instead of complex inheritance
  3. Always check the MRO when debugging
Exercise
  1. Create a diamond inheritance pattern with a base class Vehicle, two middle classes Car and Boat, and a final class AmphibiousCar. Add an identify() method to each class and use super() to chain them properly.

  2. Examine the MRO of your AmphibiousCar class and predict the output before running identify().

  3. Experiment with changing the order of inheritance (e.g., class AmphibiousCar(Boat, Car) vs class AmphibiousCar(Car, Boat)) and observe how it affects the MRO.

Super with two arguments

Most of the time you’ll use super() with no arguments, but there’s also super(ClassName, self). This does the following: look up the MRO of the object self, and get the class right after the class ClassName. This explicit form lets you control exactly where Python starts looking in the MRO:

class A:
    def greet(self):
        print("Hi from A")

class B(A):
    def greet(self):
        print("Hi from B")
        super().greet()

class C(A):
    def greet(self):
        print("Hi from C") 
        super().greet()

class D(B, C):
    def greet(self):
        print("Hi from D")
        # These are equivalent:
        super().greet()                    # Start from next in MRO  
        # super(D, self).greet()           # Same thing, explicit
d = D()
print("MRO:", [cls.__name__ for cls in D.__mro__])
d.greet()
MRO: ['D', 'B', 'C', 'A', 'object']
Hi from D
Hi from B
Hi from C
Hi from A

The two-argument form becomes useful when you want to skip certain classes in the MRO:

class D(B, C):
    def greet(self):
        print("Hi from D")
        super(B, self).greet()  # Skip B, start from C in the MRO
d = D()
d.greet()
Hi from D
Hi from C
Hi from A

This explicitly says: “Start looking for greet() from class B’s position in the MRO, but use self as the instance.” Since B comes before C in the MRO, calling super(B, self) jumps to C.

This helps you understand what super() really does: it it a shortcut for super(__class__, self), where __class__ is the class we are currently in and self is the first argument of the function we are programming. You can read more about this in PEP 3135 - New Super.

Tip

The two-argument form is rarely needed in practice. It was used in Python 2.x because the form super() didn’t exist yet. You should only use it if you have a very good reason to; otherwise stick with super().

Additional resources