Advanced Guide to Inheritance and Subclassing in Python

The Full Guide to Multiple Inheritance and Mixins in Python

Advanced Guide to Inheritance and Subclassing in Python
Inheritance in Python

Inheritance is a corner stone of any object oriented programming language, and in Python, it is as important. Only Python supports multiple inheritance too, which is the ability of a class to have more than one base class or superior class.

Today we will discuss the importance of inheritance, multiple inheritance and how and when we should use it, but first we will start with the basic super() function invocation.

What is the super() Function?

Just like Java and C#, Python supports the use of a built-in function, super(), to call its superior class and it is essential to use it consistently to well-maintain our program.

This specific method is used when a subclass overrides a method of a superclass, the overriding method usually needs to call the corresponding method of the superclass.

For example, creating a class to store items in the order they were last updated:

class LastUpdatedOrderedDict(OrderedDict):
    """Store items in the order they were last updated"""

    def __setitem__(self, key, value):
        super().__setitem__(key, value)
        self.move_to_end(key)

You see here we called the super().__setitem__(key, value), this the superclass (OrderedDict) invocation of its __setitem__ method.

Why You Should Never Subclass a Built-In Type?

Subclassing a built-in type isn't always simple, this is the case of Python's CPython implementation which is the prevalent implementation of Python to date.  Let's try something:

class AnswerDict(dict):
    def __getitem__(self, key):
        return 42

>>> ad = AnswerDict(a='foo')
>>> ad['a']
42
>>> d = {}
>>> d.update(ad)
>>> d['a']
'foo'
>>> d
{'a': 'foo'}

The issue here is that the __getitem__ of AnswerDict is bypassed by the dict.update method.  What's happening is breaking OOP rules, as looking for the method to execute should start from the subclass even if the call comes from a superclass's method, i.e. the __getitem__ of AnswerDict should execute instead we have the original __getitem__ of the superclass dict executing.

In the original author's words of the book Fluent Python about the subject:

Subclassing built-in types like dict or list or str directly is error prone because the built-in methods mostly ignore user-defined overrides. Instead of subclassing the built-ins, derive your classes from the collections module using UserDict, UserList, and UserString, which are designed to be easily extended.

Multiple Inheritance

To support multiple inheritance, programming languages should solve the Diamond Problem, this the potential conflict of 2 superclasses implementing a method with the same name.

Diamon Problem, 2 superclasses implementing methods with the same name. Image from Fluent Python Book

How the Method Resolution Order(MRO) is Decided?

In Python, Every class has an attribute called __mro__ holding a tuple of references to the superclasses in method resolution order, from the current class all the way to the object class and this attributes affects the order with which methods are called or activated:

  • The __mro__ attribute determines the activation order, i.e. which class method will be called.
  • The MRO takes into account the order in which superclasses are listed in a subclass declaration, i.e. calling Leaf(A,B) is not the same as calling Leaf(B,A), the MRO will be different (seen in example below).
  • Whether a method calls super() or not also affects the MRO, methods calling super() are called Cooperative Methods as they enable cooperative multiple inheritance, basically in order for multiple inheritance to work, Python requires the active cooperation of the methods involved, i.e. calling super().
  • The MRO is computed based on an algorithm called C3, and it is detailed here if you wanna get into more details about it.
class Root:
    def ping(self):
        print(f"{self}.ping() in Root")

    def pong(self):
        print(f"{self}.pong() in Root")

    def __repr__(self):
        cls_name = type(self).__name__
        return f"<instance of {cls_name}>"


class A(Root):
    def ping(self):
        print(f"{self}.ping() in A")
        super().ping()

    def pong(self):
        print(f"{self}.pong() in A")
        super().pong()


class B(Root):
    def ping(self):
        print(f"{self}.ping() in B")
        super().ping()

    def pong(self):
        print(f"{self}.pong() in B")


class Leaf(A, B):
    def ping(self):
        print(f"{self}.ping() in Leaf")
        super().ping()

>>> Leaf.__mro__ 
(<class 'diamond1.Leaf'>, <class 'diamond1.A'>, <class 'diamond1.B'>,
<class 'diamond1.Root'>, <class 'object'>)

>>> leaf1 = Leaf()
>>> leaf1.ping()
<instance of Leaf>.ping() in Leaf
<instance of Leaf>.ping() in A
<instance of Leaf>.ping() in B
<instance of Leaf>.ping() in Root
>>> leaf1.pong()
<instance of Leaf>.pong() in A
<instance of Leaf>.pong() in B

What are Mixin Classes?

Mixin classes are classes that are inherited along other classes, i.e. they should be subclassed with at least one other class in a multiple inheritance arrangement as they cannot offer the full functionality for a concrete class.

A Mixin Class can:

  • Add a functionality or a feature to a child class.
  • Customizes a specific behavior in a child class.
from pprint import pprint


class DictMixin:
    def to_dict(self):
        return self._traverse_dict(self.__dict__)

    def _traverse_dict(self, attributes):
        result = {}
        for key, value in attributes.items():
            result[key] = self._traverse(key, value)

        return result

    def _traverse(self, key, value):
        if isinstance(value, DictMixin):
            return value.to_dict()
        elif isinstance(value, dict):
            return self._traverse_dict(value)
        elif isinstance(value, list):
            return [self._traverse(key, v) for v in value]
        elif hasattr(value, '__dict__'):
            return self._traverse_dict(value.__dict__)
        else:
            return value


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


class Employee(DictMixin, Person):
    def __init__(self, name, skills, dependents):
        super().__init__(name)
        self.skills = skills
        self.dependents = dependents


if __name__ == '__main__':
    e = Employee(
        name='John',
        skills=['Python Programming', 'Project Management'],
        dependents={'wife': 'Jane', 'children': ['Alice', 'Bob']}
    )

    pprint(e.to_dict())
    
# {'dependents': {'children': ['Alice', 'Bob'], 'wife': 'Jane'}, 'name': 'John', 'skills': ['Python Programming', 'Project Management']}

A Mixin should:

  • Provide method implementations for reuse by multiple unrelated subclasses.
  • Provide a specific behavior or feature.
  • Never be instantiated as is.
  • Not define a new type and cannot be inherited alone.
  • Avoid keeping an internal state, this is the job of the concrete class inheriting the mixin.
  • Not have instance attributes.

Practical Guidelines when dealing with Inheritance

Inheritance and multiple inheritance can get messy and tightly couples our code and we have to be cautious when using it, here are some tips to avoid Spaghetti code situations.

Composition VS Inheritance

In the original book Design Patterns , the author talks about composition as the second principle of a good object-oriented design, specifically why we have to favor composition over inheritance.

Choosing composition leads to more flexible designs and avoids tightly coupling the code. If it's possible always go with composition, even in the case of Mixins, as composition and delegation can provide the behaviors you want to any class you want without having to inherit from a superclass.

The only case where we have to use inheritance is when we're designing our core system as inheriting from the ABC interfaces is mandatory for a good system design.

2 Reasons to Use Inheritance

When we are thinking about inheritance and where we might use it, we have to make sure that we fit one of these 2 criteria:

  • We are inheriting from an interface(usually an ABC) to create a sub-type, i.e. we have an "is-a" relationship.
  • We want to reuse code and we can make use with a Mixin or inheriting from an ABC.

Use Abstract Base Classes

If we are creating an interface, it has to be an explicit ABC, and should inherit from an ABC or multiple ABCs.

Use Aggregate Classes

Aggregate classes are classes inheriting from multiple classes and interfaces to group them together and does not add its own structure or behavior, i.e. it's a class where we inherit from different Mixins and ABCs to be used together.

What classes to subclass from?

Not all classes are designed to be subclassed! usually it is mentioned in the documentation if it can be extended or the methods are made abstract for you to implement. Needless to say, concrete classes do not fall under the category of classes that should be extended as the instances usually hold an internal state that can be corrupted.

Whenever possible, avoid overriding methods, or at least subclass classes that are meant to be easily extended, and only in the ways they were designed to be extended.

Conclusion

Inheritance is a simple concept but it isn't easy to implement it right, we tried to dive in some advanced use cases like the use of Mixins and when and why we should use inheritance but it could take a while to master the subject. Here are some resources that help me learn.

Further Reading