12.1 Inheritance Fundamentals
Inheritance lets a new class reuse another class's attributes and methods, instead
of rewriting them. The existing class is the base (or parent/superclass);
the new one is the derived (or child/subclass). This is the "is-a"
relationship — a Dog is an Animal — as opposed to chapter 11's "has-a"/"uses-a"
relationships between otherwise-unrelated objects.
Syntax: class Derived(Base):. Anything not redefined in Derived is simply inherited
as-is:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return f"{self.name} makes a sound"
class Dog(Animal):
pass # inherits __init__ and speak() unchanged - no need to redefine either
d = Dog("Rex")
print(d.name) # Rex -> inherited attribute
print(d.speak()) # Rex makes a sound -> inherited method
Rex Rex makes a sound
A Dog instance really is an Animal too — isinstance() and issubclass() both
recognize the whole chain, not just the exact class:
print(isinstance(d, Dog)) # True
print(isinstance(d, Animal)) # True -> a Dog IS an Animal too
print(issubclass(Dog, Animal)) # True
True True True
In practice — Django's abstract base models. A real Django project commonly
defines class TimestampedModel(models.Model): created_at = ...; updated_at = ...;
class Meta: abstract = True, then has every real model (Order, Product, User)
inherit from it — the exact single-inheritance shape above, reused so every model in
the app automatically gets the same timestamp fields without repeating them.
12.2 Types of Inheritance
| Type | Shape | Definition |
|---|---|---|
| Single | one base, one derived | a derived class has exactly one direct base |
| Multiple | several bases, one derived | a derived class lists more than one base at once |
| Multilevel | a chain | a derived class's base is itself derived from another base |
| Hierarchical | one base, several independent derived classes | multiple unrelated classes all derive from the same base |
| Hybrid | any combination of the above | mixes two or more of these shapes in one hierarchy |
Single inheritance — exactly one base class:
Animal
│
Dog
class Animal:
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
return "Woof!"
print(Dog().speak()) # Woof!
Woof!
Multiple inheritance — one class lists more than one base at once, class
Derived(Base1, Base2):, and gets both:
Flyer Swimmer
\ /
Duck
class Flyer:
def fly(self):
return "flying"
class Swimmer:
def swim(self):
return "swimming"
class Duck(Flyer, Swimmer):
pass
duck = Duck()
print(duck.fly(), duck.swim()) # flying swimming -> both bases' methods, at once
flying swimming
Mixins are a specific, disciplined use of multiple inheritance: a small class that
adds exactly one reusable, self-contained capability, is never meant to be instantiated
on its own, and typically carries no state of its own — no __init__, no instance
attributes — just methods that work purely off of whatever the class it's mixed into
already provides. Flyer/Swimmer above are already mixin-shaped; here's the pattern
made explicit, adding a describe() capability to any class that supplies a name:
class DescribableMixin:
"""Stateless mixin - adds one capability, assumes the host class provides self.name."""
def describe(self):
return f"This is {self.name}."
class Robot(DescribableMixin):
def __init__(self, name):
self.name = name
r = Robot("R2D2")
print(r.describe()) # This is R2D2. -> describe() came from the mixin, name from Robot itself
This is R2D2.
Prefer chapter 11's composition unless you're writing a stateless mixin like this — the moment the added behavior needs its own data or lifecycle, it stops being a mixin and is really a whole-part relationship (11.2/11.3) wearing a multiple-inheritance costume.
In practice — Django's class-based view mixins. LoginRequiredMixin and
PermissionRequiredMixin are exactly this pattern used in a real, widely-used web
framework: a view class like class OrderListView(LoginRequiredMixin, ListView):
composes its behavior from several small, independent, stateless capability mixins
instead of one deep single-inheritance chain.
Multilevel inheritance — a chain, each link derived from the one before it. A
Dog gets everything Mammal has, which in turn already includes everything Animal
has:
Animal
│
Mammal
│
Dog
class Animal2:
def eat(self):
return "eating"
class Mammal(Animal2):
def walk(self):
return "walking"
class Dog2(Mammal):
def bark(self):
return "barking"
d2 = Dog2()
print(d2.eat(), d2.walk(), d2.bark()) # eating walking barking -> all three levels reachable
eating walking barking
Hierarchical inheritance — several unrelated classes all derive from the same base, independently of each other:
Animal
/ \
Dog Cat
class Animal3:
def eat(self):
return "eating"
class Dog3(Animal3):
pass
class Cat3(Animal3):
pass
print(Dog3().eat(), Cat3().eat()) # eating eating -> same base, two independent subclasses
eating eating
Hybrid inheritance — any mix of the above in one hierarchy. Here, both B and C
derive from A (hierarchical), and D combines B and C (multiple) — a diamond
shape, since two different paths from D both lead back to the same A:
A
/ \
B C
\ /
D
This shape is exactly what makes the search order genuinely ambiguous — when D calls
a method it doesn't define itself, does Python check B's side or C's side first,
and does it visit A once or twice? That question is what the rest of this chapter
(12.4, 12.5) answers:
class A:
def a(self):
return "a"
class B(A):
def b(self):
return "b"
class C(A):
def c(self):
return "c"
class D(B, C):
def d(self):
return "d"
dd = D()
print(dd.a(), dd.b(), dd.c(), dd.d()) # a b c d -> everything reachable, from every branch
print([cls.__name__ for cls in D.__mro__]) # ['D', 'B', 'C', 'A', 'object'] -> A visited only ONCE
a b c d ['D', 'B', 'C', 'A', 'object']
12.3 Method Overriding
Overriding is when a subclass defines a method with the same name as one it
inherited, to replace that behavior instead of reusing it. The subclass's version wins
completely — the parent's version is never reached through this name, unless something
deliberately reaches back for it (which is exactly what super(), 12.4, is for):
class Animal:
def speak(self):
return "Some generic animal sound"
class Dog(Animal):
def speak(self): # same name - REPLACES the parent's version entirely
return "Woof!"
print(Animal().speak()) # Some generic animal sound
print(Dog().speak()) # Woof! -> Animal's version is never reached this way
Some generic animal sound Woof!
12.4 super() and Cooperative Inheritance
super() gives access to the next class in line — not necessarily "my direct
parent," but the next class after the current one in the object's method resolution
order (its MRO, 12.5 covers exactly how that order is computed). For single
inheritance the two happen to be the same class, which is why "super() means the
parent" is a common but incomplete mental model — 12.4's third use case below shows
exactly where it breaks down.
Use 1 — avoid duplicating a parent's __init__:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # reuse Animal's setup instead of repeating "self.name = name"
self.breed = breed
d = Dog("Rex", "Labrador")
print(d.name, d.breed) # Rex Labrador
Rex Labrador
Use 2 — extend an overridden method (12.3) instead of fully replacing it:
class Animal:
def speak(self):
return "Some generic animal sound"
class Dog(Animal):
def speak(self):
base = super().speak() # get the parent's behavior first...
return f"{base}, specifically: Woof!" # ...then build on it, instead of discarding it
print(Dog().speak()) # Some generic animal sound, specifically: Woof!
Some generic animal sound, specifically: Woof!
Use 3 — cooperative multiple inheritance: super() follows the MRO, not "my direct
parent." In a diamond (12.2), if every class's method calls super(), the calls chain
through every class in the MRO exactly once — including a class that isn't its direct
base at all:
class A:
def greet(self):
print("A.greet")
class B(A):
def greet(self):
print("B.greet")
super().greet() # B's only base is A - so surely this calls A.greet()... right?
class C(A):
def greet(self):
print("C.greet")
super().greet()
class D(B, C):
def greet(self):
print("D.greet")
super().greet()
D().greet()
# D.greet
# B.greet
# C.greet <- B's super() actually reached C, not A!
# A.greet
print([cls.__name__ for cls in D.__mro__]) # ['D', 'B', 'C', 'A', 'object']
D.greet B.greet C.greet A.greet ['D', 'B', 'C', 'A', 'object']
super() inside B.greet looks up the actual instance's MRO (D's: [D, B, C, A,
object]), finds B's own position in it, and calls whatever comes immediately
after — which is C, because D put it there, not because B knows anything about
C. This is exactly what makes multiple inheritance "cooperative": every class trusts
that calling super() will reach whichever class the final MRO says comes next, and
that every class in between plays along by also calling super().
Use 4 — the explicit two-argument form, which is what the zero-argument super()
is shorthand for:
class Animal:
def speak(self):
return "..."
class Dog(Animal):
def speak(self):
# explicit form: "starting right after Dog, in self's MRO" - super() with no
# arguments is exactly this, with Dog and self filled in automatically
return super(Dog, self).speak()
print(Dog().speak()) # ...
...
Common mistake — forgetting to forward arguments a class itself doesn't need through
a cooperative __init__ chain. The diamond above (greet(), all four Use
examples) never had to face this, because none of its methods take arguments. Real
cooperative multiple inheritance almost always does, and that's where multiple
inheritance earns its bad reputation: extending 12.2's D(B, C) diamond (MRO
[D, B, C, A, object]) with a constructor argument each class actually needs, calling
super().__init__() with only some of them breaks the chain the moment it reaches a
class further along the MRO that needed one that got dropped:
class A:
def __init__(self, a):
self.a = a
print(f"A.__init__ a={a}")
class B(A):
def __init__(self, a, b):
super().__init__(a) # forwards only 'a' - looks right if B were talking straight to A...
self.b = b
print(f"B.__init__ b={b}")
class C(A):
def __init__(self, a, c):
super().__init__(a)
self.c = c
print(f"C.__init__ c={c}")
class D(B, C):
def __init__(self, a, b, c):
super().__init__(a, b) # BUG: only passes what B's own signature wants
print("D.__init__ done")
try:
D(1, 2, 3)
except TypeError as e:
print("TypeError:", e)
# TypeError: C.__init__() missing 1 required positional argument: 'c'
# -> B's super().__init__(a) actually reaches C next (D's MRO, 12.4 Use 3),
# not A - and C needed a 'c' that was never forwarded
TypeError: C.__init__() missing 1 required positional argument: 'c'
The fix — every __init__ in the chain accepts and forwards **kwargs it doesn't
personally use, via super().__init__(**kwargs), instead of naming only the
arguments its own direct base happens to want:
class A:
def __init__(self, a, **kwargs):
self.a = a
print(f"A.__init__ a={a}")
class B(A):
def __init__(self, b, **kwargs):
super().__init__(**kwargs) # forwards whatever B itself doesn't need
self.b = b
print(f"B.__init__ b={b}")
class C(A):
def __init__(self, c, **kwargs):
super().__init__(**kwargs)
self.c = c
print(f"C.__init__ c={c}")
class D(B, C):
def __init__(self, a, b, c):
super().__init__(a=a, b=b, c=c) # pass everything as keywords; each class peels off its own
print("D.__init__ done")
d = D(a=1, b=2, c=3)
print(d.a, d.b, d.c) # 1 2 3 -> every class got exactly what it needed, in MRO order
A.__init__ a=1 C.__init__ c=3 B.__init__ b=2 D.__init__ done 1 2 3
12.5 MRO and Resolution Rules
The Method Resolution Order (MRO) is the exact, linear order Python searches classes in when looking up a method or attribute — the first class in that order that defines the name wins. Every class exposes its own MRO:
class Animal:
pass
class Dog(Animal):
pass
print(Dog.__mro__) # (<class '__main__.Dog'>, <class '__main__.Animal'>, <class 'object'>)
print(Dog.mro()) # same list, as an ordinary list instead of a tuple
(<class '__main__.Dog'>, <class '__main__.Animal'>, <class 'object'>) [<class '__main__.Dog'>, <class '__main__.Animal'>, <class 'object'>]
For single inheritance, the MRO is just the obvious chain up to object. It only gets
interesting for multiple inheritance — 12.2's diamond raised exactly this question:
does D(B, C)'s search visit B's side or C's side first, and does it reach A
once or twice? Python answers this with the C3 linearization algorithm:
L[C] = C + merge( L[B1], L[B2], ..., L[Bn], [B1, B2, ..., Bn] )
— the MRO of a class C is itself, followed by a merge of its direct bases' own
MROs, plus one more list: the bases in the order C listed them. That extra list is
what makes C's own declared base order win ties.
The merge operation takes several lists (each already-computed in order) and weaves them into one, repeating this rule until every list is empty:
- Look at the head (first element) of each list, in order.
- Pick the first head that does not appear anywhere in the tail (every element except the head) of any of the lists. That's a valid next element — promoting it now can't strand something that still has to come before it later.
- Remove that element from every list (it can only ever appear as a head at this point, never buried inside one), and append it to the result.
- If no head qualifies — every candidate is blocked by some other list's
requirements — the hierarchy has no consistent order at all, and Python raises
TypeError(demonstrated at the end of this section) instead of guessing.
Two worked examples follow: a simple diamond, then the classic textbook example that needs several rounds of merging to resolve.
Example 1 — a simple diamond:
O
/ \
A B
\ /
C
class A(O), class B(O), class C(A, B). First, the linearizations that are already
known (every class's MRO always starts with itself):
L[O] = [O, object]
L[A] = [A, O, object]
L[B] = [B, O, object]
Now L[C] = C + merge(L[A], L[B], [A, B]), worked round by round:
lists: [A, O, object] [B, O, object] [A, B]
round 1: candidate A — is A in any TAIL? [O,object]:no [O,object]:no [B]:no -> valid
take A -> result: [A]
lists: [O, object] [B, O, object] [B]
round 2: candidate O — is O in any tail? [object]:no [O,object]:YES -> blocked
candidate B — is B in any tail? [object]:no [O,object]:no []:no -> valid
take B -> result: [A, B]
lists: [O, object] [O, object] []
round 3: candidate O — is O in any tail? [object]:no [object]:no -> valid
take O -> result: [A, B, O]
lists: [object] [object]
round 4: candidate object — no tails left at all -> valid
take object -> result: [A, B, O, object]
So L[C] = C + [A, B, O, object] = [C, A, B, O, object]. Confirm against Python itself:
class O:
pass
class A(O):
pass
class B(O):
pass
class C(A, B):
pass
print([cls.__name__ for cls in C.__mro__])
# ['C', 'A', 'B', 'O', 'object'] -> matches the hand-worked trace exactly
['C', 'A', 'B', 'O', 'object']
Example 2 — a more complex hierarchy, with three separate multi-base classes
combined into one. A–E each single-inherit from O; K1, K2, K3 each combine a
different subset of them (sharing B, A and D pairwise); Z combines all three.
That triangle of shared classes can't be drawn as one flat, crossing-free picture, so
here it is as three stages instead — exactly the three merges the computation itself
needs to do:
Stage 1 — the common root:
O
┌────┬────┼────┬────┐
A B C D E
Stage 2 — each K combines its own subset of A-E:
K1(A, B, C) K2(D, B, E) K3(D, A)
A B C D B E D A
\ | / \ | / \ /
K1 K2 K3
Stage 3 — Z combines all three K's:
K1 K2 K3
\ | /
Z
Stage 1 needs no merge at all — each of A–E single-inherits directly from O,
so their linearizations are just the obvious chain:
L[O] = [O, object]
L[A] = [A, O, object] L[B] = [B, O, object] L[C] = [C, O, object]
L[D] = [D, O, object] L[E] = [E, O, object]
Stage 2 is where the first real merges happen. L[K1] = K1 + merge(L[A], L[B],
L[C], [A, B, C]), worked round by round exactly like Example 1:
lists: [A, O, object] [B, O, object] [C, O, object] [A, B, C]
round 1: candidate A — in any tail? no, no, no, no -> valid
take A -> result: [A]
lists: [O, object] [B, O, object] [C, O, object] [B, C]
round 2: candidate O — in any tail? no, YES (2nd list) -> blocked
candidate B — in any tail? no, no, no -> valid
take B -> result: [A, B]
lists: [O, object] [O, object] [C, O, object] [C]
round 3: candidate O — in any tail? no, YES (3rd list) -> blocked
candidate C — in any tail? no, no, no -> valid
take C -> result: [A, B, C]
lists: [O, object] [O, object] [O, object]
round 4: candidate O — in any tail? no, no, no -> valid
take O -> result: [A, B, C, O]
lists: [object] [object] [object]
round 5: candidate object — no tails left -> valid
take object -> result: [A, B, C, O, object]
So L[K1] = [K1, A, B, C, O, object]. L[K2] = K2 + merge(L[D], L[B], L[E], [D, B,
E]) follows the identical shape (just relabeled — B is blocked in round 2 for the
same reason B was blocked above, since it's K2's own shared class this time):
lists: [D, O, object] [B, O, object] [E, O, object] [D, B, E]
round 1: candidate D — in any tail? no, no, no, no -> valid
take D -> result: [D]
lists: [O, object] [B, O, object] [E, O, object] [B, E]
round 2: candidate O — in any tail? no, YES (2nd list) -> blocked
candidate B — in any tail? no, no, no -> valid
take B -> result: [D, B]
lists: [O, object] [O, object] [E, O, object] [E]
round 3: candidate O — in any tail? no, YES (3rd list) -> blocked
candidate E — in any tail? no, no, no -> valid
take E -> result: [D, B, E]
lists: [O, object] [O, object] [O, object]
round 4: candidate O — in any tail? no, no, no -> valid
take O -> result: [D, B, E, O]
lists: [object] [object] [object]
round 5: candidate object — no tails left -> valid
take object -> result: [D, B, E, O, object]
So L[K2] = [K2, D, B, E, O, object]. L[K3] = K3 + merge(L[D], L[A], [D, A]) is the
shortest, since K3 only combines two classes:
lists: [D, O, object] [A, O, object] [D, A]
round 1: candidate D — in any tail? no, no, no -> valid
take D -> result: [D]
lists: [O, object] [A, O, object] [A]
round 2: candidate O — in any tail? no, YES (2nd list) -> blocked
candidate A — in any tail? no, no -> valid
take A -> result: [D, A]
lists: [O, object] [O, object]
round 3: candidate O — in any tail? no, no -> valid
take O -> result: [D, A, O]
lists: [object] [object]
round 4: candidate object — no tails left -> valid
take object -> result: [D, A, O, object]
So L[K3] = [K3, D, A, O, object].
Stage 3 — the main event. L[Z] = Z + merge(L[K1], L[K2], L[K3], [K1, K2, K3]),
using the three results just computed:
lists: [K1,A,B,C,O,object] [K2,D,B,E,O,object] [K3,D,A,O,object] [K1,K2,K3]
round 1: candidate K1 — in any tail? no, no, no, no -> valid
take K1 -> result: [K1]
lists: [A,B,C,O,object] [K2,D,B,E,O,object] [K3,D,A,O,object] [K2,K3]
round 2: candidate A — in any tail? YES (3rd list has A in its tail) -> blocked
candidate K2 — in any tail? no, no, no, no -> valid
take K2 -> result: [K1, K2]
lists: [A,B,C,O,object] [D,B,E,O,object] [K3,D,A,O,object] [K3]
round 3: candidate A — in any tail? YES (3rd list) -> blocked
candidate D — in any tail? YES (3rd list has D in its tail too) -> blocked
candidate K3 — in any tail? no, no, no -> valid
take K3 -> result: [K1, K2, K3]
lists: [A,B,C,O,object] [D,B,E,O,object] [D,A,O,object]
round 4: candidate A — in any tail? YES (3rd list) -> blocked
candidate D — in any tail? no, no, no -> valid
take D -> result: [..., D]
lists: [A,B,C,O,object] [B,E,O,object] [A,O,object]
round 5: candidate A — in any tail? no, no, no -> valid
take A -> result: [..., A]
lists: [B,C,O,object] [B,E,O,object] [O,object]
round 6: candidate B — in any tail? no, no, no -> valid
take B -> result: [..., B]
lists: [C,O,object] [E,O,object] [O,object]
round 7: candidate C — in any tail? no, no, no -> valid
take C -> result: [..., C]
lists: [O,object] [E,O,object] [O,object]
round 8: candidate O — in any tail? no, YES (2nd list) -> blocked
candidate E — in any tail? no, no, no -> valid
take E -> result: [..., E]
lists: [O,object] [O,object] [O,object]
round 9: candidate O — in any tail? no, no, no -> valid
take O -> result: [..., O]
lists: [object] [object] [object]
round 10: candidate object — no tails left -> valid
take object -> result: [K1,K2,K3,D,A,B,C,E,O,object]
So L[Z] = Z + [K1,K2,K3,D,A,B,C,E,O,object] = [Z, K1, K2, K3, D, A, B, C, E, O,
object] — worked out entirely by hand, matching Python's own answer. D (round 4) is
the interesting one: it's buried inside K3's own list until K3 itself is taken in
round 3, so it can only become a valid candidate one round later. Confirm all four
linearizations against Python directly:
class O: pass
class A(O): pass
class B(O): pass
class C(O): pass
class D(O): pass
class E(O): pass
class K1(A, B, C): pass
class K2(D, B, E): pass
class K3(D, A): pass
class Z(K1, K2, K3): pass
print([cls.__name__ for cls in K1.__mro__]) # ['K1', 'A', 'B', 'C', 'O', 'object']
print([cls.__name__ for cls in K2.__mro__]) # ['K2', 'D', 'B', 'E', 'O', 'object']
print([cls.__name__ for cls in K3.__mro__]) # ['K3', 'D', 'A', 'O', 'object']
print([cls.__name__ for cls in Z.__mro__])
# ['Z', 'K1', 'K2', 'K3', 'D', 'A', 'B', 'C', 'E', 'O', 'object'] -> matches every hand-worked trace
['K1', 'A', 'B', 'C', 'O', 'object'] ['K2', 'D', 'B', 'E', 'O', 'object'] ['K3', 'D', 'A', 'O', 'object'] ['Z', 'K1', 'K2', 'K3', 'D', 'A', 'B', 'C', 'E', 'O', 'object']
All four hand traces line up exactly. That bookkeeping is also just code — here is the merge rule from 12.5 as a literal, runnable implementation, so it can be pointed at any hierarchy instead of redone by hand every time:
def merge(lists, verbose=False):
lists = [list(lst) for lst in lists if lst] # drop empty lists up front
result = []
round_num = 1
while lists:
candidate = None
for lst in lists:
head = lst[0]
if not any(head in other[1:] for other in lists): # rule 2: not in any TAIL
candidate = head
break
if candidate is None:
raise TypeError(f"Cannot create a consistent MRO from {lists}") # rule 4
if verbose:
print(f"round {round_num}: pick {candidate.__name__:<6} "
f"lists={[[c.__name__ for c in lst] for lst in lists]}")
result.append(candidate) # rule 3
lists = [[c for c in lst if c is not candidate] for lst in lists]
lists = [lst for lst in lists if lst]
round_num += 1
return result
def linearize(cls, verbose=False):
if not cls.__bases__:
return [cls]
# verbose is NOT passed to the recursive calls - only THIS class's own merge is traced
return [cls] + merge([linearize(base) for base in cls.__bases__] + [list(cls.__bases__)], verbose=verbose)
class O: pass
class A(O): pass
class B(O): pass
class C(O): pass
class D(O): pass
class E(O): pass
class K1(A, B, C): pass
class K2(D, B, E): pass
class K3(D, A): pass
class Z(K1, K2, K3): pass
computed = linearize(Z, verbose=True)
print([cls.__name__ for cls in computed])
print(computed == list(Z.__mro__)) # True -> our from-scratch implementation matches Python exactly
round 1: pick K1 lists=[['K1', 'A', 'B', 'C', 'O', 'object'], ['K2', 'D', 'B', 'E', 'O', 'object'], ['K3', 'D', 'A', 'O', 'object'], ['K1', 'K2', 'K3']] round 2: pick K2 lists=[['A', 'B', 'C', 'O', 'object'], ['K2', 'D', 'B', 'E', 'O', 'object'], ['K3', 'D', 'A', 'O', 'object'], ['K2', 'K3']] round 3: pick K3 lists=[['A', 'B', 'C', 'O', 'object'], ['D', 'B', 'E', 'O', 'object'], ['K3', 'D', 'A', 'O', 'object'], ['K3']] round 4: pick D lists=[['A', 'B', 'C', 'O', 'object'], ['D', 'B', 'E', 'O', 'object'], ['D', 'A', 'O', 'object']] round 5: pick A lists=[['A', 'B', 'C', 'O', 'object'], ['B', 'E', 'O', 'object'], ['A', 'O', 'object']] round 6: pick B lists=[['B', 'C', 'O', 'object'], ['B', 'E', 'O', 'object'], ['O', 'object']] round 7: pick C lists=[['C', 'O', 'object'], ['E', 'O', 'object'], ['O', 'object']] round 8: pick E lists=[['O', 'object'], ['E', 'O', 'object'], ['O', 'object']] round 9: pick O lists=[['O', 'object'], ['O', 'object'], ['O', 'object']] round 10: pick object lists=[['object'], ['object'], ['object']] ['Z', 'K1', 'K2', 'K3', 'D', 'A', 'B', 'C', 'E', 'O', 'object'] True
Reading the trace: K1, K2, K3 are taken immediately (rounds 1–3) since Z
listed them in that order and nothing blocks it yet. D (round 4) has to wait one
round — it's buried inside K3's list on round 3 (behind K3 itself), so it only
becomes a valid head once K3 is removed. From there the merge threads through A,
B, C, E in the order their positions force, and O/object bring up the rear,
since every list still needs them last.
When no consistent order exists, the merge rule's step 4 kicks in — Python detects this and refuses to guess:
try:
class X:
pass
class Y(X):
pass
class Z2(X, Y): # X listed before Y, but Y already requires X to come before IT
pass
except TypeError as e:
print("TypeError:", e)
# TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y
# our own merge() agrees, for the same reason - no head is ever valid:
try:
merge([linearize(X), linearize(Y), [X, Y]])
except TypeError as e:
print("raised:", type(e).__name__) # raised: TypeError
TypeError: Cannot create a consistent method resolution order (MRO) for bases X, Y raised: TypeError
Practical summary. Everything from 12.5's formula down through the
merge()/linearize() implementation is the actual algorithm — worth tracing by hand
once, so "the MRO" stops being a black box. Day to day, nobody re-derives C3
linearization at the keyboard: when a multiple-inheritance hierarchy behaves
unexpectedly, check Cls.__mro__ (or Cls.mro()) directly and read off the order
Python already computed for you — you almost never need to compute it by hand
yourself.
12.6 Abstract Classes and Interfaces
A plain "template" base class only enforces its contract by convention — nothing stops someone from instantiating it directly, or from a subclass forgetting to implement the method it promised:
class Shape:
def area(self):
raise NotImplementedError("subclasses must implement area()")
s = Shape() # succeeds - nothing stops direct instantiation of a "template"
try:
s.area() # the mistake only surfaces here, if/when area() is actually called
except NotImplementedError as e:
print("NotImplementedError:", e)
NotImplementedError: subclasses must implement area()
The abc module makes this an actual, enforced rule instead of a comment: inherit
from ABC and mark required methods with @abstractmethod. Python then refuses to
create an instance of the class itself, or of any subclass that hasn't implemented
every abstract method — the mistake is caught immediately, not on first use:
from abc import ABC, abstractmethod
class ShapeABC(ABC):
@abstractmethod
def area(self):
... # no body - just a required signature every subclass must fill in
try:
ShapeABC() # the class itself can never be instantiated
except TypeError as e:
print("TypeError:", e)
class Incomplete(ShapeABC):
pass # forgot to implement area()
try:
Incomplete() # caught immediately, before any method is ever called
except TypeError as e:
print("TypeError:", e)
TypeError: Can't instantiate abstract class ShapeABC without an implementation for abstract method 'area' TypeError: Can't instantiate abstract class Incomplete without an implementation for abstract method 'area'
A subclass that implements every abstract method behaves like any ordinary class — this
is what makes ShapeABC effectively an interface: a contract other classes commit
to, rather than something ever used on its own:
In practice — a swappable-backend/strategy architecture. A StorageBackend ABC
with LocalStorage/S3Storage/GCSStorage subclasses, or a PaymentProcessor ABC
with StripeProcessor/PaypalProcessor subclasses, is exactly this pattern used to
build real, swappable infrastructure: application code depends only on the abstract
interface, so adding a new backend later means writing one new subclass, not touching
any of the code that already uses the interface.
class Circle(ShapeABC):
def __init__(self, radius):
self.radius = radius
def area(self): # implements the required method - now instantiable
return 3.14159 * self.radius ** 2
c = Circle(2)
print(c.area()) # 12.56636
12.56636
Abstract properties — @property (10.6) and @abstractmethod stack together, to
require not just a method but a specific attribute-like piece of the interface:
Note — decorator order matters here. Decorators apply bottom-up, so
@property has to be the outermost (topmost) one: it needs to wrap the
already-abstract method and turn the whole thing into a property descriptor,
propagating the "still abstract" flag onto itself in the process. Reversed
(@abstractmethod on top of @property), abstractmethod() would instead try to mark
an already-built property object as abstract directly, which raises an
AttributeError — property objects don't allow that attribute to be set after the
fact.
class NamedShape(ABC):
@property
@abstractmethod
def name(self):
...
@abstractmethod
def area(self):
...
class Square(NamedShape):
def __init__(self, side):
self.side = side
@property
def name(self):
return "Square"
def area(self):
return self.side ** 2
sq = Square(4)
print(sq.name, sq.area()) # Square 16
Square 16
Going deeper — ABC inheritance is the nominal answer to "interface"; Python also
has a structural one. Requiring ShapeABC/NamedShape as an explicit base class
only recognizes objects that formally opted in through inheritance. A lot of idiomatic
Python instead leans on duck typing ("if it walks like a duck and quacks like a
duck...") — trusting any object with the right methods, inheritance or not.
typing.Protocol makes that checkable without demanding real
inheritance: a Protocol subclass just describes the required shape, and static type
checkers (plus isinstance(), for protocols marked @runtime_checkable) can confirm
an unrelated class satisfies it structurally. ABC.register() sits in between — it
retroactively declares an already-existing, unrelated class a "virtual subclass" of an
ABC (so isinstance()/issubclass() say yes) without touching that class's actual
inheritance at all. In a lot of real-world code, one of these is the more idiomatic
answer to "interface" than writing a fresh ABC hierarchy from scratch.