Skip to main content
  1. Readings/
  2. Books/
  3. Fluent Python: Clear, Concise, and Effective Programming – Luciano Ramalho/

Chapter 14. Inheritance: For Better or for Worse

··2562 words·13 mins

Focus areas for this chapter:

  • The super() function
  • The pitfalls of subclassing from built-in types
  • Multiple inheritance and method resolution order
  • Mixin classes

chapter introduces multiple inheritance for those who have never used it, and provides some guidance on how to cope with single or multiple inheritance if you must use it.

What’s New in This Chapter #

The super() Function #

  • example use cases

    • when a subclass overrides a method of a superclass

      and we want to let the superclass method do its job then add more logic to it

    • when we let the superclasses do their part in init fns

  • LANG_LIMITATION / IDIOM: unlike java constructor that automatically calls the nullary super constructor, python doesn’t do this so we need to ALWAYS manually write this in.

    1
    2
    3
    
      def __init__(self, a, b) :
              super().__init__(a, b)
              ... # more initialization code
    
    • it will work (but not recommended) for us to hardcode the base class and call that base class’s function.

      Also won’t work well with the multiple inheritance stuff

Subclassing Built-In Types Is Tricky #

  • Main takeaway:

    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.

    • it’s a flaw in method delegation within the C Language code of the builtin types (only affects classes derived directly from those types).
  • Major Caveat: bypassing behaviour

    the code of the built-ins (written in C) usually does not call methods overridden by user-defined classes.

    this applies for other dunder methods calling the overriden method.

    using the overriding method directly is likely to work still.

  • This built-in behavior is a violation of a basic rule of object-oriented programming: the search for methods should always start from the class of the receiver (self), even when the call happens inside a method implemented in a superclass.

  • virtual vs nonvirtual methods

    virtual: late-bound

    non-virtual: bound at compile time

    in python, every method is like latebound like a vritual method

    builtins written in C seem to be nonvirtual by default (at least in CPython).

Multiple Inheritance and Method Resolution Order #

  • guiding question:

    if we do multiple inheritance and both super classes have overlapping method names, how to make reference to the correct super function from the subclass

    \(\implies\) this is the diamond problem and we wanna see how python solves this

  • 2 factors that determined the activation sequences:

    1. MRO of the leaf class

      Goes all the way from current class all the way to the object class

      Defines the activation order

    2. use of super() in each method

      Determines whether a particular method will be activated.

      So if the method calls super() then we move to the next class in the MRO order and execute that.

      How?

      It’s not necessarily a BFS, it uses the C3 Algorithm (not important to understand unless need to wrangle complex hierarchies.)

  • MRO accounts for inheritance graph. Amongst siblings, it’s determined by the subclass declaration.

    e.g. Leaf(B, A), Leaf(A, B) are two different subclass declarations.

  • Cooperative Methods: methods that call super()

    Cooperative methods enable cooperative multiple inheritance. These terms are intentional: in order to work, multiple inheritance in Python requires the active cooperation of the methods involved.

    GOTCHA: cooperative methods can be a cause of subtle bugs. \(\implies\) That’s why it is recommended that every method m of a nonroot class should call super().m().

A noncooperative method can be the cause of subtle bugs.

Manycoders reading Example 14-4 may expect that when method A.pong calls super.pong(), that will ultimately activate Root.pong. But if B.pong is activated before, it drops the ball.

That’s why it is recommended that every method m of a nonroot class should call super().m().

Mixin Classes #

  • definition:

    • designed to be sub classed together with at least one other class as part of a multiple inheritance arrangement

    • won’t provide all the functionality of a concrete object

    • it’s supposed to be functionality mixins \(\implies\) customizes the behaviour of child or sibling classes.

      so naturally will have some concrete methods implemented

    • are a convention that has no explicit language support in python/cpp

  • Mixins must appear first in the tuple of base classes in a class declaration

    mixins typically depend on sibling classes that implements / inherits methods with the same signature

    therefore, they must appear early in the MRO of a subclass that uses it

Case-Insensitive Mappings #

See this beautiful example

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
"""
Short demos
===========

``UpperDict`` behaves like a case-insensitive mapping`::

# tag::UPPERDICT_DEMO[]
    >>> d = UpperDict([('a', 'letter A'), (2, 'digit two')])
    >>> list(d.keys())
    ['A', 2]
    >>> d['b'] = 'letter B'
    >>> 'b' in d
    True
    >>> d['a'], d.get('B')
    ('letter A', 'letter B')
    >>> list(d.keys())
    ['A', 2, 'B']

# end::UPPERDICT_DEMO[]

And ``UpperCounter`` is also case-insensitive::

# tag::UPPERCOUNTER_DEMO[]
    >>> c = UpperCounter('BaNanA')
    >>> c.most_common()
    [('A', 3), ('N', 2), ('B', 1)]

# end::UPPERCOUNTER_DEMO[]

Detailed tests
==============

UpperDict uppercases all string keys.

    >>> d = UpperDict([('a', 'letter A'), ('B', 'letter B'), (2, 'digit two')])


Tests for item retrieval using `d[key]` notation::

    >>> d['A']
    'letter A'
    >>> d['b']
    'letter B'
    >>> d[2]
    'digit two'


Tests for missing key::

    >>> d['z']
    Traceback (most recent call last):
      ...
    KeyError: 'Z'
    >>> d[99]
    Traceback (most recent call last):
      ...
    KeyError: 99


Tests for item retrieval using `d.get(key)` notation::

    >>> d.get('a')
    'letter A'
    >>> d.get('B')
    'letter B'
    >>> d.get(2)
    'digit two'
    >>> d.get('z', '(not found)')
    '(not found)'

Tests for the `in` operator::

    >>> ('a' in d, 'B' in d, 'z' in d)
    (True, True, False)

Test for item assignment using lowercase key::

    >>> d['c'] = 'letter C'
    >>> d['C']
    'letter C'

Tests for update using a `dict` or a sequence of pairs::

    >>> d.update({'D': 'letter D', 'e': 'letter E'})
    >>> list(d.keys())
    ['A', 'B', 2, 'C', 'D', 'E']
    >>> d.update([('f', 'letter F'), ('G', 'letter G')])
    >>> list(d.keys())
    ['A', 'B', 2, 'C', 'D', 'E', 'F', 'G']
    >>> d  # doctest:+NORMALIZE_WHITESPACE
    {'A': 'letter A', 'B': 'letter B', 2: 'digit two',
    'C': 'letter C', 'D': 'letter D', 'E': 'letter E',
    'F': 'letter F', 'G': 'letter G'}

UpperCounter uppercases all `str` keys.

Test for initializer: keys are uppercased.

    >>> d = UpperCounter('AbracAdaBrA')
    >>> sorted(d.keys())
    ['A', 'B', 'C', 'D', 'R']

Tests for count retrieval using `d[key]` notation::

    >>> d['a']
    5
    >>> d['z']
    0

"""
# tag::UPPERCASE_MIXIN[]
import collections

def _upper(key):  # <1>
    try:
        return key.upper()
    except AttributeError:
        return key

class UpperCaseMixin:  # <2>
    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))
# end::UPPERCASE_MIXIN[]

# tag::UPPERDICT[]
class UpperDict(UpperCaseMixin, collections.UserDict):  # <1>
    pass

class UpperCounter(UpperCaseMixin, collections.Counter):  # <2>
    """Specialized 'Counter' that uppercases string keys"""  # <3>
# end::UPPERDICT[]

Multiple Inheritance in the Real World #

Usage of multiple inheritance is not the norm in cpp or python really.

There are some known good use cases though.

ABCs Are Mixins Too #

  • Just like Java, we support multiple inheritance of interfaces

  • for those ABCs that have concrete methods, they play 2 roles:

    1. they play the role of an interface definition

    2. they also play the role of mixin classes

ThreadingMixIn and ForkingMixIn #

Django Generic Views Mixins #

  • the original generic views were functions so they were not extensible

    have to start from scratch if we want to do something similar but not the same

  • Concrete subclasses of View are supposed to implement the handler methods, so why aren’t those methods part of the View interface? The reason: subclasses are free to implement just the handlers they want to support. A TemplateView is used only to display content, so it only implements get

    reminder on why:

      Great question! What you’re touching on is a common and important aspect of Python’s interface and subclassing philosophy, especially in frameworks like Django (which uses **View** classes).
    
      ### In Python, do you have to implement *all* methods of an interface/abstract base class?
    
      **Short answer:**
      **Not necessarily.** Unlike some statically typed languages with strict interface contracts, Python’s interfaces and abstract base classes (ABCs) often allow (or even expect) subclasses to implement only the parts of the interface that make sense for them.
    
      ### Why?
    
      1. **Python’s dynamic and flexible nature**
    ​     - Python favors *duck typing* and optional implementation over strict enforcement.
    ​     - ABCs in Python can define optional methods, or provide default no-op or generic implementations in base classes.
    
      2. **Partial method implementation is common in frameworks**
    ​     - Consider a `View` base class that defines many “handler methods” like `get()`, `post()`, `put()`, etc.
    ​     - Concrete subclasses (e.g., `TemplateView`, `CreateView`) implement only the handlers relevant to the use case.
    ​     - This lets you write minimal classes that do exactly what you need, without burden.
    
      3. **Interface methods can be *optional* or *protocol-like***
    ​     - Instead of forcing every subclass to implement an entire interface, the base class or the framework’s machinery calls a method *if it exists*.
    ​     - If the method is not implemented, either a default behavior occurs, or a runtime error only arises if the method is actually called.
    
      4. **Explicit design: selective handler implementation**
    ​     - The `TemplateView` subclass only implements the `get()` method because it only responds to HTTP GET requests.
    ​     - If you called `post()` on a `TemplateView`, it might raise a `NotImplementedError` or return a “method not allowed” response.
    ​     - This selective implementation is a common design pattern in web frameworks.
    
      ### Contrast to stricter languages or interfaces
    
      In languages like Java or C#, interfaces typically require **all** declared methods to be implemented. Python’s philosophy is more permissive:
    
      - **You can think of many interfaces or ABCs in Python as “soft” contracts.**
    ​  - **You implement the “part you use.”**
    ​  - This works well because Python’s runtime dispatch is dynamic: calls to methods that don’t exist will fail only if you exercise them.
    
      ### Summary
    
      - In Python, you **do not always have to implement every method** defined by an interface or abstract base class.
    ​  - Interfaces or base classes often define *optional* or *selective* methods.
    ​  - Subclasses implement only the methods relevant to their behavior.
    ​  - This design promotes flexibility and simplicity in code, especially in toolkits like Django where views handle different HTTP methods.
    ​  - Calling a method that’s not implemented typically leads to a runtime error only if that method is invoked.
    
      If you want, I can provide a small Python example illustrating this partial implementation pattern (e.g., a base class with several methods, and subclasses implementing only some of them). Would that be helpful?
    
  • It does take some time to learn how to leverage class-based views and how to extend them to fulfill specific application needs, but I found that it was worthwhile to study them.

    Value that the mixins give for Django:

    They eliminate a lot of boilerplate code, make it easier to reuse solutions, and even improve team communication—for example, by defining standard names to templates, and to the variables passed to template contexts. Class-based views are Django views “on rails.”

Multiple Inheritance in Tkinter #

  • the class heirarchy SHOULD NOT be very deep

    usually it’s around 3 or 4 levels of concrete classes

  • GUI toolkits are where inheritance is most useful. The hierarchies can get really deep in them.

⭐️ Coping with Inheritance #

These are the rules of thumb that we need to rely on.

We have to do so because there’s no general theory about inheritance that can guide us against creating incomprehensible, brittle designs.

Favor Object Composition over Class Inheritance #

  • do composition and delegation

    it can even replace the use of mixins and make behaviours available to different classes.

  • subclassing is a form of tight coupling and tall inheritance trees tend to be brittle.

Understand Why Inheritance Is Used in Each Case #

Reasons FOR using inheritance:

  1. creates a subtype, so it’s a is-a relationship best done with ABCs

  2. avoids code duplication by reuse, Mixins are useful for this too

The realisation here is that to prevent code reuse, inheritance is only an implementation detail, we can do composition & delegation too. However, interface inheritance is separate matter.

Make Interfaces Explicit with ABCs #

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

Use Explicit Mixins for Code Reuse #

  • for reuse by multiple unrelated subclasses, without implying an “is-a” relationship,
  • not to be instantatied
  • since there’s no formal convention, try to Suffix the mixin name with Mixin

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.

  • group together combinations of ABCs or mixins

    we can now just use the aggregate class without having to figure out in which order they should be declared to work as intended.

  • typically just has an empty body (with docstring / pass)

Subclass Only Classes Designed for Subclassing #

  • some superclass methods may ignore the subclass overrides in unexpected ways.

    \(\implies\) we should subclass only those that are intended to be extended.

  • how to check?

    • see the docs, if it’s a base class named, that hints at it

    • the docs will also indicate which of the methods are intended to be overridden.

    • see if the @final decorator exists on the method (then it’s not intended for extension by overriding that method)

Avoid Subclassing from Concrete Classes #

  • if you do this, any internal state within a concrete class might get corrupted

    even if we coorperate by calling super(), there’s still many ways bugs can be introduced

  • If you must use subclassing for code reuse, then the code intended for reuse should be in mixin methods of ABCs or in explicitly named mixin classes.

Tkinter: The Good, the Bad, and the Ugly #

Chapter Summary #

Further Reading #

  • Smalltalk has traits which are a language construct that serves the role that a mixin class does, while avoiding some of the issues with multiple inheritance.

    Scala also has traits.

  • while working as an application developer, you find yourself building multilevel class hierarchies, it’s likely that one or more of the following applies:

    1. You are reinventing the wheel. Go look for a framework or library that provides

    2. components you can reuse in your application.

    3. You are using a badly designed framework. Go look for an alternative.

    4. You are overengineering. Remember the KISS principle.

    5. You became bored coding applications and decided to start a new framework.

      Congratulations and good luck!

  • Subclassing in Python Redux