Skip to content

Inheritance: For Better of for Worseđź”—

  • multiple inheritance is supported in C++, Java but not C#. After percieved abuse in early C++ codebases, Java left it out.
  • there is significant backlash against overuse of inheritance, multiple inheritance (worse), because superclasses and subclasses are tightly coupled.
  • after all this bad name, its(multiple inheritance) still usefull in many practical scenarios, like in standard library, Django, Tkinter GUI toolkit.

The super() Functionđź”—

  • consistent use of the super() built-in function is essential for maintainable object-oriented Python programs.
  • when subclass method overrides a method of a superclass, the overriding method usually needs to call the corresponding method of superclass.
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)
def __init__(self, a, b):
  super().__init__(a,b)
  ... # more init code
  • there are few examples where overriding method doesn’t use super() but instead call method directly as OrderedDict.__setitem__(self, key, value). This is not recommended for reasons
    • hardcoding base class name
    • super implements logic to handle class hierarchies with multiple inheritance.

Subclassing Built-In Types Is Trickyđź”—

  • earlier versions of python didn’t support subclassing built-ins (most are implemented in C) usually doesn’t call methods overridden by user-defined classes.
  • From official documentation:
    • CPython has no rule at all for when exactly overridden method of subclasses of built-in types get implicitly called or not. As an approximation, these methods are never called by other built-in methods of the same object. For example, an overridden __getitem__() in a subclass of dict will not be called by e.g. the built-in get() method.
class DoppelDict(dict):
  def __setitem__(self, key, value):
    super().__setitem__(key, [value] * 2)       # duplicate values when storing

dd = DoppelDict(one = 1)    # constructor ignores __setitem__
dd  # {'one': 1}
dd['two'] = 2   # [] operator works
dd  # {'one': 1, 'two': [2,2]}
dd.update(three = 3)    # update doesn't work expected
dd  # {'three': 3, 'one' : 1, 'two':[2,2]}
  • The built-in behaviour is a violation of a basic rule of OOP: the search for methods should always start from class of the receiver(self), even when the call happens inside a method implemented in a superclass. This is what is called late binding which is a key feature of OOP. In any call of form x.method() the exact method to be called must be determined at runtime, based on class of receiver x.
  • The problem is not limited to calls within an instance—whether self.get() calls self.__getitem__()—but also happens with overridden methods of other classes that should be called by the built-in methods.
class AnswerDict(dict):
  def __getitem__(self, key):   # always return 42
    return 42

ad = AnswerDict(a = 'foo')  # loaded with (a='foo')
ad['a'] # 42    # expected
d = {}
d.update(ad)    # update normal dict d, updated with ad
d['a']  # 'foo' # but our `dict.update` ignored our original AnswerDict method
d           # {'a':'foo'}

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 and Method Resolution Orderđź”—

  • Any language which implements multiple inheritance has to deal with naming conflicts when superclass implement a method by the same name, commonly known as diamond problem.

UML for diamond problem

Assume following implementation

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()
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

Lets take a look at MRO of the class to find references to its superclasses

Leaf.__mro__  # doctest:+NORMALIZE_WHITESPACE
    (<class 'diamond1.Leaf'>, <class 'diamond1.A'>, <class 'diamond1.B'>,
     <class 'diamond1.Root'>, <class 'object'>)
  • MRO only determines activation order, but when particular method will be activated in each of the classes depend on whether each implementation calls super() or not.
  • In pong method. The Leaf doesn’t override it, therefore activated implementation of next class Leaf.__mro__ : the A class. Since method A.pong calls super().pong(). The B class is next in MRO, therefore B.pong is activated but method doesn’t calls super.pong() so activation stops.
  • NOTE: declaration Leaf(A,B) and Leaf(B, A) are separate and determine relative order in MRO.
  • when a method calls super(), it is a cooperative method. Cooperative methods enable cooperative multiple inheritance. These terms are intension in order to work, multiple inheritance in Python requires cooperation of the methods involved. In the B class, ping cooperates but pong doesn’t.
  • Python is a dynamic language, so the interaction of super() with the MRO is also dynamic.
from diamond import A
class U():
  def ping(self):
    print(f'{self}.ping() in U')
    super().ping()

class LeafUA(U, A):
  def ping(self):
    print(f'{self}.ping() in LeafUA')
    super().ping()
u = U()
u.ping()
# AttributeError: 'super' object has no attribute 'ping'
# because notice U doesn't inherit any class except object, which doesn't implement ping

leaf2 = LeafUA()    # this works as MRO works fine, last Root class doesn't call super ending MRO traversal
  • Classes like U are called as mixin class. Its a class intended to be used together with other cleasses in multiple inheritance, to provide additional functionality.

Mixin Classesđź”—

A mixin class is designed to be subclassed together with at least one other class in a multiple inheritance arrangement.

  • A mixin is not supposed to be only base class of a concrete class, because it doesn’t provide all the functionality for a concrete class.
  • only add/customizes the behaviour of child or sibling class.

Case-Insesitive Mappingsđź”—

import collections

def _upper(key):    # helper function
    try:
        return key.upper()
    except AttributeError:
        return key

class UpperCaseMixin: # mixin implement 4 essential methods of mappings, always calling `super()` with the `key` uppercased
    def __setitem__(self, key, item):
        super().__setitem__(_upper(key), item)

    def __getitem__(self, key):
        return super().__getitem__(_upper(key))

    def get(self, key, default=None):
        return super().get(_upper(key), default)

    def __contains__(self, key):
        return super().__contains__(_upper(key))
# Concrete Classes using above Mixin
class UpperDict(UpperCaseMixin, collections.UserDict): # UserDict
    pass

class UpperCounter(UpperCaseMixin, collections.Counter): # Counter
    """Specialized 'Counter' that uppercases string keys"""
d = UpperDict([('a', 'letter A'), (2, 'digit two')])
list(d.keys()) # ['A', 2]
d['b'] = 'letter B'
'b' in d    # true
list(d.keys())  # ['A', 2, 'B']

Multiple Inheritance in the Real Worldđź”—

  • In Design Patterns book, almost all code is in C++, but the only example of multiple inheritance is the Adapter Pattern, In python, multiple inhertance is not the norm either but there are important examples.

ABCs Are Mixins Toođź”—

  • documentation of collection.abc uses the term mixin method for the concrete methods implemented in many of the collection ABCs.
  • The ABCs that provide mixin methods play two roles
    • Interface definitions
    • Mixin Classes

ThreadingMixIn and ForkingMixInđź”—

The http.server package provides HTTPServer and ThreadingHTTPServer. Latter was added in 3.7+, is identical to HTTPServer but uses threads to handle requests by using the ThreadingMixIn. This is useful to handle web browsers pre-opening sockets, on which HTTPServer would wait indefinitely

Complete Source Code for it is

class ThreadingHTTPServer(socketserver.ThreadingMixin, HTTPServer)
    daemon_threads = True

Django Generic Views Mixinsđź”—

  • In Django, a view is a callable object that takes a request argument—an object representing an HTTP request—and returns an object representing an HTTP response.
  • View is the base class of all views (it could be an ABC), and it provides core functionality like the dispatch method, which delegates to “handler” methods like get, head, post, etc., implemented by concrete subclasses to handle the different HTTP verbs. The RedirectView class inherits only from View, and you can see that it implements get, head, post, etc.

Multiple Inheritance in Tkinterđź”—

  • Extreme example of multiple inheritance is Python’s standard library, Tkinter GUI Toolkit.
  • By current standards, the class hierarchy of Tkinter is very deep. Few parts of the Python standard library have more than three or four levels of concrete classes, and the same can be said of the Java class library. However, it is interesting to note that the some of the deepest hierarchies in the Java class library are precisely in the packages related to GUI programming:

Coping with Inheritanceđź”—

What Alan Kay wrote in the epigraph remains true: there’s still no general theory about inheritance that can guide practicing programmers. What we have are rules of thumb, design patterns, “best practices,” clever acronyms, taboos, etc. Some of these provide useful guidelines, but none of them are universally accepted or always applicable.

here are a few tips to avoid spaghetti class graphs.

Favor Object Composition over Class Inheritanceđź”—

  • second principle of object-oriented design from the Design Patterns book
  • Favoring composition leads to more flexible designs.
  • Composition and delegation can replace the use of mixins to make behaviors available to different classes, but cannot replace the use of interface inheritance to define a hierarchy of types.

Understand Why Inheritance is Used in Each Caseđź”—

  • When dealing with multiple inheritance, it’s useful to keep straight the reasons why subclassing is done in each particular case. The main reasons are:
    • Inheritance of interface creates a subtype, implying an “is-a” relationship. This is best done with ABCs.
      • Example: Consider a scenario where you have a base class Shape, and you want to create specific shapes like Circle and Rectangle. Each shape must implement certain methods like area() and perimeter(). In this case, Shape can be an abstract base class defining these methods, and Circle and Rectangle can be concrete subclasses implementing these methods.
    • Inheritance of implementation avoids code duplication by reuse. Mixins can help with this.
      • Suppose you have multiple classes (A, B, C) that need logging functionality. Instead of duplicating logging code in each class, you can create a mixin class LogMixin that contains the logging functionality. Then, you can inherit from LogMixin in each class that needs logging capabilities.

Make Interface Explicit with ABCsđź”—

In modern Python, if a class is intended to define an interface, it should be an explicit ABC or a typing.Protocol subclass. An ABC should subclass only abc.ABC or other ABCs. Multiple inheritance of ABCs is not problematic.

Use Explicit Mixins for Code Reuseđź”—

If a class is designed to provide method implementations for reuse by multiple unrelated subclasses, without implying an “is-a” relationship, it should be an explicit mixin class. Conceptually, a mixin does not define a new type; it merely bundles methods for reuse. A mixin should never be instantiated, and concrete classes should not inherit only from a mixin. Each mixin should provide a single specific behavior, implementing few and very closely related methods. Mixins should avoid keeping any internal state; i.e., a mixin class should not have instance attributes.

  • There is no formal way in Python to state that a class is a mixin, so it is highly recommended that they are named with a Mixin suffix.

Provide Aggregate Classes to Usersđź”—

A class that is constructed primarily by inheriting from mixins and does not add its own structure or behavior is called an aggregate class.

If some combination of ABCs or mixins is particularly useful to client code, provide a class that brings them together in a sensible way.

Subclass Only Classes Designed for Subclassingđź”—

Subclassing any complex class and overriding its methods is error-prone because the superclass methods may ignore the subclass overrides in unexpected ways. As much as possible, avoid overriding methods, or at least restrain yourself to subclassing classes which are designed to be easily extended, and only in the ways in which they were designed to be extended.

he PEP introduces a @final decorator that can be applied to classes or individual methods, so that IDEs or type checkers can report misguided attempts to subclass those classes or override those methods.

Avoid Subclassing from Concrete Classesđź”—

Subclassing concrete classes is more dangerous than subclassing ABCs and mixins, because instances of concrete classes usually have internal state that can easily be corrupted when you override methods that depend on that state. Even if your methods cooperate by calling super(), and the internal state is held in private attributes using the __x syntax, there are still countless ways a method override can introduce bugs.

Tkinter: The Good, the Bad, and the Uglyđź”—

Most advice in the previous section is not followed by Tkinter, with the notable exception of “Provide Aggregate Classes to Users”.

Keep in mind that Tkinter has been part of the standard library since Python 1.1 was released in 1994. Tkinter is a layer on top of the excellent Tk GUI toolkit of the Tcl language. The Tcl/Tk combo is not originally object-oriented, so the Tk API is basically a vast catalog of functions. However, the toolkit is object-oriented in its design, if not in its original Tcl implementation.

To be fair, as a Tkinter user, you don’t need to know or use multiple inheritance at all. It’s an implementation detail hidden behind the widget classes that you will instantiate or subclass in your own code. But you will suffer the consequences of excessive multiple inheritance when you type dir(tkinter.Button) and try to find the method you need among the 214 attributes listed. And you’ll need to face the complexity if you decide to implement a new Tk widget.