Gradient AtlasML Materials

Data science roadmap › Python for Data Science › Chapter 12

Inheritance and Method Resolution

Type hierarchies, overriding, cooperative superclass calls and the MRO.

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:

In [1]13 lines
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:

In [2]3 lines
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
In [3]9 lines
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
In [4]13 lines
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:

In [5]11 lines
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
In [6]14 lines
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
In [7]11 lines
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:

In [8]20 lines
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):

In [9]10 lines
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__:

In [10]11 lines
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:

In [11]10 lines
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:

In [12]26 lines
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:

In [13]11 lines
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:

In [14]29 lines
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:

In [15]24 lines
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:

In [16]8 lines
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:

  1. Look at the head (first element) of each list, in order.
  2. 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.
  3. 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.
  4. 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:

In [17]14 lines
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:

In [18]16 lines
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:

In [19]42 lines
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:

In [20]16 lines
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:

In [21]9 lines
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:

In [22]19 lines
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.

In [23]9 lines
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.

In [24]23 lines
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.