Skip to main content

Inheritance: For Good or For Worse

Source: Fluent Python, 2nd Edition — Chapter 14 by Luciano Ramalho

Python-specific traps: subclassing built-ins (their C methods skip your overrides) and multiple inheritance (who wins when two parents define pong?). Java skipped MI and still won; many Python users never saw it until Tkinter or Django. Frameworks still force hierarchies. A growing camp — Go, composition-first talks — says avoid inheritance when you can. This chapter is how it works when you cannot.

Subclassing built-ins is tricky​

Since 2.2 you can subclass list / dict / str. CPython has no clean rule for when an override runs. Approximation: other built-in methods of the same object do not call your override. dict.get will not use your __getitem__. __init__ and update will not use your __setitem__. That breaks “always start lookup from the instance’s class.” __missing__ is a documented special case, not a general promise.

class DoppelDict(dict):
def __setitem__(self, key, value):
super().__setitem__(key, [value] * 2)

DoppelDict(one=1) # {'one': 1} — __init__ ignored you
d = DoppelDict(); d['two'] = 2 # [2, 2] — [] uses __setitem__
d.update(three=3) # 3, not [3, 3]

AnswerDict.__getitem__ always returns 42, but plain.update(answer_dict) copies the stored 'foo', not 42.

Fix: subclass collections.UserDict, UserList, or UserString (Python wrappers that delegate). Or implement a collections.abc mapping/sequence yourself. A UserDict StrKeyDict stayed ~16 lines; the same tests against raw dict needed __init__, get, and update as well (~37 lines).

This pitfall is C built-ins calling each other. Python-coded bases (UserDict, MutableMapping) cooperate.

If you inherit dict instead, update leaves a bare 2 / 9 and the tests fail.

Quiz

Built-in subclasses

1 / 1

d.update(ad) when ad is a dict subclass with __getitem__ always 42

Pick an answer to see if you are right.

Multiple inheritance and MRO​

Two unrelated bases with the same method: the diamond (both inherit A, D(B, C)). C++ makes you qualify. Python uses C3 MRO, stored as Cls.__mro__. class D(B, C) searches D, B, C, A, object. Swap to D(C, B) and C wins over B.

d.pong() follows MRO. C.pong(d) skips MRO (unbound method; pass self yourself). Prefer super() on code you do not own — it walks this instance’s MRO, not “my literal parent.”

d.pingpong()
self.ping() → D.ping → super().ping() → A.ping, then post-ping
super().ping() → A.ping (skips D)
self.pong() → B.pong
super().pong() → B.pong again
C.pong(self) → C.pong (uppercase), ignores MRO

bool.__mro__ is bool, int, object. FrenchDeck2 walks MutableSequence → Sequence → Sized → Iterable → Container. io.TextIOWrapper walks text/IO ABCs. You do not need to memorize C3 unless the graph is messy; Simionato’s paper is there if it is.

Tkinter Text.__mro__ is a long chain: Widget, geometry mixins Pack/Place/Grid, XView/YView. Adapter in GoF uses MI; the other 22 patterns do not. collections.abc is the uncontroversial case (Java-like interfaces plus mixin methods).

self.pong() must be pong (B). The explicit C.pong(self) must still be PONG.

Coping with multiple inheritance​

Kay: inheritance mixes reuse, specialization, and too many other jobs. Tips:

  1. Interface (“is-a”) vs implementation (reuse). Reuse is often better as composition.
  2. Interfaces → explicit ABCs.
  3. Reuse without a type → mixin: not instantiated alone; not the only base of a concrete class; few related methods.
  4. Name mixins FooMixin. Tkinter’s Pack / XView do not.
  5. An ABC may mixin concrete helpers; a mixin is not an ABC (no subtype, not a sole base). ABC helpers may only use the ABC’s own interface.
  6. At most one concrete superclass. The rest ABCs or mixins.
  7. Aggregate classes: empty body that stacks a useful combo (tkinter.Widget; Django ListView).
  8. Favor composition. A widget is not a geometry manager; it could hold one. New managers would not fight MRO. Composition cannot replace interface inheritance for a type lattice.

Tkinter: cautionary​

1994 wrapper around non-OO Tcl/Tk. Misc has 100+ methods on every widget (clipboard on a scrollbar). Widget is an aggregate of BaseWidget + three geometry classes — useful (#7), but composition would be cleaner. dir(Button) is 200+ names. Users can still ship real UIs (ttk, Canvas, Text). You inherit the mess anyway.

Django class-based views: clearer mixins​

A view: request in, response out. Old generic views were functions (not extensible). 1.3 added classes. View dispatches HTTP verbs; missing post → 405. TemplateResponseMixin only for template views — RedirectView skips it. ListView is an empty aggregate: View + MultipleObjectMixin (query + pagination) + template mixin. BaseListView is the JSON-shaped hole: list data, no template. Mixins are named *Mixin and have a job. Some teams still write one giant function; learning the CBV “rails” cuts boilerplate.

Classy Class-Based Views is the map.

super() is not the villain; MI is complicated. Hettinger vs Knight. Traits (Simionato) constrain mixins. Regebro: MI’s dangers are overblown in practice. Rhodes: composition over inheritance. Fackler/Manista: interfaces + functions. Go: no class inheritance at all.

When the framework is a tree, you still need MRO and mixins. When you own the design, start with composition.

Quiz

Mixins and aggregates

1 / 2

A concrete class should inherit from

Pick an answer to see if you are right.