Multiple Inheritance and the MRO

A class can inherit from several parents. The method resolution order decides which version wins, and C3 linearisation is the algorithm that computes it.

Two parents

class Swimmer:
    def move(self):
        return "swimming"

    def breathe(self):
        return "gills"


class Walker:
    def move(self):
        return "walking"

    def legs(self):
        return 4


class Amphibian(Swimmer, Walker):
    pass


a = Amphibian()
print(a.move())        # swimming - Swimmer is listed first
print(a.breathe())     # gills
print(a.legs())        # 4 - inherited from Walker

The class gained everything from both parents. Where both define the same name, the leftmost parent wins.

The method resolution order

print([cls.__name__ for cls in Amphibian.__mro__])
# ['Amphibian', 'Swimmer', 'Walker', 'object']

Amphibian.mro()        # the same thing as a method
help(Amphibian)        # shows the MRO too

The MRO is a single ordered list of classes. Every attribute lookup walks it from left to right and stops at the first match. There is no ambiguity at runtime, ever - the order was decided when the class was created.

The diamond

        A
       / \
      B   C
       \ /
        D
class A:
    def hello(self):
        return "A"


class B(A):
    def hello(self):
        return "B -> " + super().hello()


class C(A):
    def hello(self):
        return "C -> " + super().hello()


class D(B, C):
    def hello(self):
        return "D -> " + super().hello()


print(D().hello())                              # D -> B -> C -> A
print([cls.__name__ for cls in D.__mro__])      # ['D', 'B', 'C', 'A', 'object']
Look at what happened inside B.hello. Its super() called C, not A, even though C is not B's parent. super() means "the next class in the MRO of the actual object", not "my parent". This is the single most important fact about multiple inheritance in Python.

Note also that A.hello ran exactly once. A naive depth first search would have visited it twice.

C3 linearisation

The MRO is computed by an algorithm called C3 linearisation. Three rules define it:

  1. A class always comes before all of its parents.
  2. Parents keep the left to right order they were declared in.
  3. Each class appears exactly once.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
class E(C, B): pass          # the parents in the other order

print([c.__name__ for c in D.__mro__])     # ['D', 'B', 'C', 'A', 'object']
print([c.__name__ for c in E.__mro__])     # ['E', 'C', 'B', 'A', 'object']

When no order is possible

class A: pass
class B(A): pass

# class Broken(A, B): pass
# TypeError: Cannot create a consistent method resolution order (MRO)
#            for bases A, B

Rule 1 requires B before A; rule 2 requires A before B. No ordering satisfies both, so Python refuses to create the class. The error arrives at class definition time, not later.

Cooperative inheritance

class Base:
    def __init__(self, **kwargs):
        self.log = []
        super().__init__(**kwargs)      # pass anything left along


class Timestamped(Base):
    def __init__(self, created_at=None, **kwargs):
        super().__init__(**kwargs)
        from datetime import date
        self.created_at = created_at or date.today()


class Tagged(Base):
    def __init__(self, tags=None, **kwargs):
        super().__init__(**kwargs)
        self.tags = tags or []


class Note(Timestamped, Tagged):
    def __init__(self, text, **kwargs):
        super().__init__(**kwargs)
        self.text = text

    def __repr__(self):
        return f"Note({self.text!r}, tags={self.tags}, created={self.created_at})"


n = Note("first note", tags=["draft"])
print(n)
print([c.__name__ for c in Note.__mro__])

Every __init__ in the chain runs exactly once, in MRO order. Three rules make cooperative inheritance work:

  • Every class calls super().__init__(...).
  • Every class accepts **kwargs and passes on what it does not consume.
  • There is a common base at the bottom that stops the chain.
class Broken(Base):
    def __init__(self, value, **kwargs):
        self.value = value
        # no super() call - everything after this class in the MRO is skipped

Mixins

A mixin is a small class that adds one capability and is never instantiated on its own. It is the most defensible use of multiple inheritance.

import json


class ReprMixin:
    """Adds a readable repr based on the instance dictionary."""

    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{type(self).__name__}({fields})"


class JsonMixin:
    """Adds JSON serialisation."""

    def to_json(self, **kwargs):
        return json.dumps(vars(self), default=str, **kwargs)


class ComparableMixin:
    """Compares instances by their attribute dictionaries."""

    def __eq__(self, other):
        return type(self) is type(other) and vars(self) == vars(other)


class Product(ReprMixin, JsonMixin, ComparableMixin):
    def __init__(self, name, price):
        self.name = name
        self.price = price


p = Product("Notebook", 250)
print(p)
print(p.to_json())
print(p == Product("Notebook", 250))     # True

Mixin conventions

  • Name it ending in Mixin.
  • Put mixins before the main base class, so they can override it.
  • Give each one a single capability.
  • Do not give a mixin its own __init__ unless it cooperates properly with super().
  • Never instantiate a mixin directly.

Inspecting a hierarchy

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass


print(D.__bases__)                            # the direct parents
print([c.__name__ for c in D.__mro__])        # the full lookup order
print(D.__subclasses__())                     # direct children
print(issubclass(D, A), isinstance(D(), A))   # True True

# Where does a name actually come from?
for cls in D.__mro__:
    if "hello" in cls.__dict__:
        print("hello is defined in", cls.__name__)
        break

Common mistakes

  • Assuming super() means "my parent". It means "the next class in the MRO".
  • Omitting a super() call, silently skipping the rest of the chain.
  • Calling Parent.__init__(self, ...) directly, which breaks cooperative inheritance.
  • Not accepting **kwargs in a cooperative __init__, so arguments cannot pass through.
  • Listing base classes in an order that has no consistent MRO.
  • Using multiple inheritance where composition would be clearer.

Best practices

  • Prefer composition; use multiple inheritance mainly for mixins.
  • Print __mro__ whenever the resolution order is not obvious.
  • Make every class in a cooperative chain call super() and accept **kwargs.
  • Keep mixins small, single purpose and stateless.
  • Put mixins first in the base class list.

Practice

  1. Build a diamond hierarchy and print the MRO, predicting it first.
  2. Write four classes where each __init__ cooperates, and prove each runs once.
  3. Create an inconsistent MRO deliberately and read the error message.
  4. Write three mixins and combine them into one class.
  5. Given a class with five ancestors, write code that reports which one defines a given method.

Conclusion

Multiple inheritance is unambiguous in Python because the MRO is a single ordered list computed once. super() follows that list rather than the class tree, cooperative __init__ chains depend on every class calling it, and mixins are where all of this genuinely pays off.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.