Python inheritance, explained
Inheritance is easy until multiple inheritance enters the room — then super(), the MRO,
and the diamond problem decide who answers a method call. Python has a precise, deterministic
answer to all of it (the C3 linearisation), and understanding it is what separates a shallow
from a deep OOP answer.
Single vs multiple inheritance
A class can inherit from one base (single) or several (multiple):
class Animal:
def speak(self):
return "..."
class Dog(Animal): # single inheritance
def speak(self):
return "Woof"
class Amphibious(Car, Boat): # multiple inheritance
pass
Single inheritance models "is-a" cleanly. Multiple inheritance is powerful but needs the MRO to stay sane.
The Method Resolution Order (MRO)
The MRO is the ordered list of classes Python searches when looking up an attribute or method. It's computed by the C3 linearisation algorithm, which guarantees a consistent order where subclasses come before their parents:
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
D.__mro__ # (D, B, C, A, object)
D.mro() # same thing as a list
A lookup on a D instance checks D, then B, then C, then A, then object, stopping
at the first match.
How super() really works
A common misconception: super() does not mean "my parent class". It means "the next
class in the MRO after the current one". In single inheritance those coincide; in
multiple inheritance they don't:
class A:
def __init__(self):
print("A")
class B(A):
def __init__(self):
print("B")
super().__init__() # calls the NEXT in the MRO, not necessarily A
class C(A):
def __init__(self):
print("C")
super().__init__()
class D(B, C):
def __init__(self):
print("D")
super().__init__()
D() # prints D, B, C, A
Notice B.__init__'s super() calls C, not A, because C is next in D's MRO. This
cooperative behaviour is exactly what makes the diamond work.
The diamond problem
The "diamond" is when two parents share a common grandparent (D inherits from B and C,
both from A). The danger is calling the shared base twice. Python's MRO solves this:
each class appears exactly once in the MRO, so cooperative super() calls run A's
__init__ a single time:
# In the example above, A printed once — not twice —
# even though both B and C inherit from it.
The rule for making this work: every class in the hierarchy should call super().__init__()
(and accept/forward **kwargs), so the chain isn't broken.
Mixins
A mixin is a class that provides a focused piece of behaviour to be "mixed in" via multiple inheritance — it isn't meant to be instantiated alone:
class JsonMixin:
def to_json(self):
import json
return json.dumps(self.__dict__)
class User(JsonMixin):
def __init__(self, name):
self.name = name
User("Ada").to_json() # '{"name": "Ada"}'
Mixins keep cross-cutting capabilities (serialisation, comparison, logging) separate from the main class hierarchy.
Abstract base classes vs mixins
They're often confused. An abstract base class defines an interface with
@abstractmethod and can't be instantiated until subclasses implement those methods. A
mixin provides working behaviour. ABCs say "you must implement this"; mixins say "here,
have this".
Recap
Python supports single and multiple inheritance, and resolves attributes via the MRO
computed by C3 linearisation (Cls.__mro__). super() delegates to the next class in
the MRO, not literally the parent — which is why cooperative super().__init__() calls
make the diamond problem safe, running each shared base exactly once. Use mixins to
add focused, reusable behaviour through multiple inheritance, and reserve abstract base
classes for defining interfaces that subclasses must implement.