Skip to Content
Course content

120: Multiple Inheritance and the MRO

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Multiple inheritance is one of those Python features that looks powerful on paper but can quickly turn into a nightmare if you don't understand how Python decides which method to actually run. We're talking about the Method Resolution Order, or MRO. To make sense of this, let's build a character system for a tabletop RPG.

Defining the combatant hierarchy

I want a base class for any entity that can fight, and then specialized classes for different roles. Let's start simple. Every Combatant has a name, and then we'll have Warrior and Mage subclasses that add their own specific stats.

class Combatant:
    def __init__(self, name):
        print("Initializing Combatant")
        self.name = name

class Warrior(Combatant):
    def __init__(self, name, strength):
        print("Initializing Warrior")
        Combatant.__init__(self, name)
        self.strength = strength

class Mage(Combatant):
    def __init__(self, name, intelligence):
        print("Initializing Mage")
        Combatant.__init__(self, name)
        self.intelligence = intelligence

At this point, everything feels normal. I'm explicitly calling the parent's __init__ method. It's clear, it's direct, and it works for single inheritance. But here is where I'm about to walk into a trap.

The hybrid Spellblade and the double-init bug

Now, I want to create a Spellblade—a character who is both a Warrior and a Mage. Since Python allows multiple inheritance, I'll just list both as parents.

class Spellblade(Warrior, Mage):
    def __init__(self, name, strength, intelligence, weapon):
        print("Initializing Spellblade")
        Warrior.__init__(self, name, strength)
        Mage.__init__(self, name, intelligence)
        self.weapon = weapon

# Let's see what happens
sb = Spellblade("Valerius", 15, 15, "Runeblade")

If you run this, the output is:

  • Initializing Spellblade
  • Initializing Warrior
  • Initializing Combatant
  • Initializing Mage
  • Initializing Combatant

Wait. Why did Initializing Combatant print twice? This is a classic mistake. Because I called Warrior.__init__ and Mage.__init__ explicitly, and both of those called Combatant.__init__, the base class was initialized twice. In a real app, this could mean resetting a database connection twice or clearing a list you just populated. It's inefficient and dangerous.

Fixing the flow with super()

To fix this, we need to stop calling parent classes by name and start using super(). A common misconception is that super() just calls the "parent." In reality, it calls the next class in the MRO. For super() to work in a multiple inheritance scenario, every class in the chain must use it.

class Combatant:
    def __init__(self, name, **kwargs):
        print("Initializing Combatant")
        self.name = name

class Warrior(Combatant):
    def __init__(self, strength, **kwargs):
        print("Initializing Warrior")
        super().__init__(**kwargs)
        self.strength = strength

class Mage(Combatant):
    def __init__(self, intelligence, **kwargs):
        print("Initializing Mage")
        super().__init__(**kwargs)
        self.intelligence = intelligence

class Spellblade(Warrior, Mage):
    def __init__(self, name, strength, intelligence, weapon):
        print("Initializing Spellblade")
        # We pass the specific args for the next classes in the MRO
        super().__init__(name=name, strength=strength, intelligence=intelligence)
        self.weapon = weapon

I've added **kwargs here because super() doesn't know which parent is next in line. By using keyword arguments, Warrior can pluck out the strength it needs and pass the rest (like intelligence and name) up the chain until they reach the class that actually needs them.

Visualizing the MRO

If you're wondering how Python knows that Warrior comes before Mage, it's using an algorithm called C3 Linearization. You don't need to memorize the math, but you should know how to inspect the result. Every Python class has an __mro__ attribute.

print(Spellblade.__mro__)
# Output: (, , , , )

This is the "search list." When you call a method, Python looks in Spellblade. Not there? It checks Warrior. Not there? Mage. Then Combatant. Finally, it hits object (the root of all Python classes). This linear path is what prevents the "diamond problem" where a base class might be visited multiple times.




📋 Practical Task

Building a Technomancer Class Hierarchy

You need to implement a character system for a Sci-Fi RPG. Create the following class structure ensuring that the base class is only initialized once:

  • A base class Entity that initializes a name.
  • A class Hacker that inherits from Entity and initializes coding_skill.
  • A class Psionic that inherits from Entity and initializes mind_power.
  • A hybrid class Technomancer that inherits from both Hacker and Psionic, and initializes a device.

Requirements:

  1. Use super() in all __init__ methods.
  2. Use **kwargs to ensure arguments are passed correctly through the MRO.
  3. Print a unique message in each __init__ (e.g., "Init Hacker") so you can verify the execution order.
  4. Instantiate a Technomancer and print its __mro__ to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.