Your game character needs to be Movable, Damageable, and Renderable. Python lets a class inherit from multiple parents. The Method Resolution Order (MRO) determines which parent's method is used when names collide.

Basic multiple inheritance

Inherit from two parent classes.

basic_multiple.py
# Basic Multiple Inheritance

class Swimmer:
    """Can swim."""

    def swim(self):
        return f"{self.__class__.__name__} is swimming"

    def describe(self):
        return "I can swim"


class Flyer:
    """Can fly."""

    def fly(self):
        return f"{self.__class__.__name__} is flying"

    def describe(self):
        return "I can fly"


class Walker:
    """Can walk."""

    def walk(self):
        return f"{self.__class__.__name__} is walking"

    def describe(self):
        return "I can walk"


# Single inheritance - one parent
class Fish(Swimmer):
    pass


# Multiple inheritance - two parents
class Duck(Swimmer, Flyer, Walker):
    """Ducks can swim, fly, and walk!"""
    pass


# Another multiple inheritance example
class Penguin(Swimmer, Walker):
    """Penguins can swim and walk, but not fly."""
    pass


def main():
    print("=== Basic Multiple Inheritance ===\n")

    # Single inheritance
    print("--- Single Inheritance (Fish) ---")
    fish = Fish()
    print(fish.swim())
    print(f"describe(): {fish.describe()}")

    # Multiple inheritance - Duck
    print("\n--- Multiple Inheritance (Duck) ---")
    duck = Duck()
    print(duck.swim())
    print(duck.fly())
    print(duck.walk())
    print(f"describe(): {duck.describe()}")

    # Multiple inheritance - Penguin
    print("\n--- Multiple Inheritance (Penguin) ---")
    penguin = Penguin()
    print(penguin.swim())
    print(penguin.walk())
    print(f"describe(): {penguin.describe()}")

    # Check what methods are available
    print("\n--- Available Methods ---")
    print(f"Duck methods: swim(), fly(), walk(), describe()")
    print(f"Penguin methods: swim(), walk(), describe()")

    # Check inheritance
    print("\n--- Inheritance Check (isinstance) ---")
    print(f"duck is Swimmer: {isinstance(duck, Swimmer)}")
    print(f"duck is Flyer: {isinstance(duck, Flyer)}")
    print(f"duck is Walker: {isinstance(duck, Walker)}")
    print(f"penguin is Swimmer: {isinstance(penguin, Swimmer)}")
    print(f"penguin is Flyer: {isinstance(penguin, Flyer)}")

    # Parent classes
    print("\n--- Parent Classes (__bases__) ---")
    print(f"Duck parents: {Duck.__bases__}")
    print(f"Penguin parents: {Penguin.__bases__}")

    print("\n=== Key Points ===")
    print("""
    1. class Child(Parent1, Parent2) inherits from both
    2. Child has access to all parent methods
    3. isinstance() checks all parent types
    4. __bases__ shows direct parent classes
    5. When methods conflict, leftmost parent wins
    """)

main()














































class Child(Parent1, Parent2): inherits from both. Child has all methods.

multiple inheritance Inherit from multiple classes: `class C(A, B):`. Gets methods from both.

Method Resolution Order

How Python decides which method to call.

mro.py
# Method Resolution Order (MRO)

class A:
    def method(self):
        return "A.method()"

    def who_am_i(self):
        return "I am A"


class B(A):
    def method(self):
        return "B.method()"


class C(A):
    def method(self):
        return "C.method()"

    def who_am_i(self):
        return "I am C"


class D(B, C):
    pass  # Inherits from both B and C


class E(C, B):
    pass  # Same parents, different order!


def main():
    print("=== Method Resolution Order (MRO) ===\n")

    # Display the inheritance structure
    print("Inheritance Structure:")
    print("""
          A
         / \\
        B   C
         \\ /
          D
    """)

    # Check MRO for D
    print("--- MRO for class D(B, C) ---")
    print(f"D.__mro__ = {D.__mro__}")
    print("\nMRO as list:")
    for i, cls in enumerate(D.__mro__):
        print(f"  {i + 1}. {cls.__name__}")

    # Method resolution demonstration
    print("\n--- Method Resolution for D ---")
    d = D()
    print(f"d.method() = {d.method()}")
    print("  → Found in B (first parent with method)")

    print(f"d.who_am_i() = {d.who_am_i()}")
    print("  → Not in D, not in B, found in C")

    # Compare with E (different parent order)
    print("\n--- MRO for class E(C, B) ---")
    print(f"E.__mro__ = {E.__mro__}")

    e = E()
    print(f"\ne.method() = {e.method()}")
    print("  → Found in C (first parent with method)")

    print(f"e.who_am_i() = {e.who_am_i()}")
    print("  → Found in C (first parent with method)")

    # Using mro() method
    print("\n--- Using mro() method ---")
    print(f"D.mro() = {D.mro()}")

    # MRO for simpler case
    print("\n--- MRO for Simple Cases ---")
    print(f"B.__mro__ = {B.__mro__}")
    print("  B → A → object")

    print(f"C.__mro__ = {C.__mro__}")
    print("  C → A → object")

    # Demonstrate search path
    print("\n--- How Python Finds Methods ---")
    print("When calling d.method():")
    print("  1. Look in D        → Not found")
    print("  2. Look in B        → FOUND! Stop here")
    print("  3. (Would look in C)")
    print("  4. (Would look in A)")
    print("  5. (Would look in object)")

    print("\nWhen calling d.who_am_i():")
    print("  1. Look in D        → Not found")
    print("  2. Look in B        → Not found")
    print("  3. Look in C        → FOUND! Stop here")

    # Key points about MRO
    print("\n=== C3 Linearization Rules ===")
    print("""
    1. Children come before parents
    2. Left parents come before right parents
    3. A class appears only once in MRO
    4. Each class must appear before its parents
    5. Follows C3 linearization algorithm
    """)

main()





































ClassName.__mro__ shows lookup order. Follows C3 linearization algorithm.

MRO Method Resolution Order. Determines which parent's method is used.

The diamond problem

When two parents share a grandparent.

diamond_problem.py
# The Diamond Problem

# The "diamond" comes from the inheritance shape:
#       A
#      / \
#     B   C
#      \ /
#       D

class A:
    def __init__(self):
        print("A.__init__() called")
        self.value = "from A"

    def greet(self):
        return "Hello from A"

    def identify(self):
        return f"A (value={self.value})"


class B(A):
    def __init__(self):
        print("B.__init__() called")
        super().__init__()  # Calls next in MRO
        self.b_attr = "from B"

    def greet(self):
        return "Hello from B"


class C(A):
    def __init__(self):
        print("C.__init__() called")
        super().__init__()  # Calls next in MRO
        self.c_attr = "from C"

    def greet(self):
        return "Hello from C"


class D(B, C):
    def __init__(self):
        print("D.__init__() called")
        super().__init__()  # Starts the chain


def main():
    print("=== The Diamond Problem ===\n")

    # Show the diamond structure
    print("Diamond inheritance structure:")
    print("""
          A (base)
         / \\
        B   C
         \\ /
          D
    """)

    # The problem: A's __init__ called twice?
    print("--- Without super() properly, A could be initialized twice ---")
    print("With super() and MRO, A is initialized only ONCE!\n")

    # Create D instance - watch the init order
    print("Creating D():")
    print("-" * 30)
    d = D()
    print("-" * 30)

    # Show the MRO
    print(f"\nD's MRO: {[cls.__name__ for cls in D.__mro__]}")

    # Explain the init chain
    print("\n--- Init Chain Explanation ---")
    print("""
    D.__init__() called
    ↓ super().__init__() → next in MRO is B
    B.__init__() called
    ↓ super().__init__() → next in MRO is C (not A!)
    C.__init__() called
    ↓ super().__init__() → next in MRO is A
    A.__init__() called
    ✓ A is initialized only ONCE!
    """)

    # Check attributes
    print("--- Attributes from all classes ---")
    print(f"d.value = '{d.value}'   (from A)")
    print(f"d.b_attr = '{d.b_attr}'  (from B)")
    print(f"d.c_attr = '{d.c_attr}'  (from C)")

    # Method resolution in diamond
    print("\n--- Method Resolution in Diamond ---")
    print(f"d.greet() = '{d.greet()}'")
    print("  → D has no greet(), check MRO")
    print("  → B has greet() → 'Hello from B'")

    print(f"\nd.identify() = '{d.identify()}'")
    print("  → Only A has identify()")

    # Bad approach: calling parent explicitly
    print("\n--- Bad Approach (Don't Do This) ---")
    print("""
    # Instead of super(), calling parents directly:
    class D(B, C):
        def __init__(self):
            B.__init__(self)  # A initialized here
            C.__init__(self)  # A initialized AGAIN!

    This would initialize A twice!
    Always use super() in multiple inheritance.
    """)

    # Why super() works
    print("=== Why super() Works ===")
    print("""
    1. super() follows MRO, not just parent
    2. In D(B, C): super() in B calls C, not A
    3. Each class is initialized exactly once
    4. Order: D → B → C → A → object
    5. This is called "cooperative multiple inheritance"
    """)

main()

































A → B, C → D. Python's MRO ensures each class is called once.

diamond problem Ambiguity when same method inherited through multiple paths. MRO resolves it.

super() chain

Cooperative multiple inheritance.

super_chain.py
# Using super() with Multiple Inheritance

class Base:
    def __init__(self, **kwargs):
        print(f"Base.__init__(kwargs={kwargs})")
        # End of chain - absorb remaining kwargs

    def process(self):
        print("Base.process()")
        return ["Base"]


class FeatureA(Base):
    def __init__(self, a_param="default_a", **kwargs):
        print(f"FeatureA.__init__(a_param={a_param}, kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass remaining kwargs up
        self.a_param = a_param

    def process(self):
        print("FeatureA.process()")
        result = super().process()  # Call next in chain
        return ["FeatureA"] + result


class FeatureB(Base):
    def __init__(self, b_param="default_b", **kwargs):
        print(f"FeatureB.__init__(b_param={b_param}, kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass remaining kwargs up
        self.b_param = b_param

    def process(self):
        print("FeatureB.process()")
        result = super().process()  # Call next in chain
        return ["FeatureB"] + result


class FeatureC(Base):
    def __init__(self, c_param="default_c", **kwargs):
        print(f"FeatureC.__init__(c_param={c_param}, kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass remaining kwargs up
        self.c_param = c_param

    def process(self):
        print("FeatureC.process()")
        result = super().process()  # Call next in chain
        return ["FeatureC"] + result


class Combined(FeatureA, FeatureB, FeatureC):
    def __init__(self, **kwargs):
        print(f"Combined.__init__(kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass all kwargs

    def process(self):
        print("Combined.process()")
        result = super().process()  # Start the chain
        return ["Combined"] + result


def main():
    print("=== super() Chain with **kwargs ===\n")

    # Show MRO
    print("MRO for Combined:")
    print([cls.__name__ for cls in Combined.__mro__])
    print()

    # Create with all parameters
    print("--- Creating Combined with all parameters ---")
    obj = 

    print(f"\nAttributes:")
    print(f"  obj.a_param = '{obj.a_param}'")
    print(f"  obj.b_param = '{obj.b_param}'")
    print(f"  obj.c_param = '{obj.c_param}'")

    # Process chain demonstration
    print("\n--- Calling process() ---")
    result = obj.process()
    print(f"\nResult: {result}")

    # Create with default parameters
    print("\n" + "=" * 50)
    print("--- Creating Combined with defaults ---")
    obj2 = Combined()

    print(f"\nAttributes (defaults):")
    print(f"  obj2.a_param = '{obj2.a_param}'")
    print(f"  obj2.b_param = '{obj2.b_param}'")
    print(f"  obj2.c_param = '{obj2.c_param}'")

    # Explain the pattern
    print("\n=== The **kwargs Pattern ===")
    print("""
    Each class:
    1. Accepts its own named parameter
    2. Accepts **kwargs for other classes
    3. Uses super().__init__(**kwargs)

    This lets each class extract its parameter
    and pass the rest up the chain!

    Example:
    Combined(a_param="A", b_param="B", c_param="C")

    → Combined receives: {a_param="A", b_param="B", c_param="C"}
    → FeatureA takes a_param, passes: {b_param="B", c_param="C"}
    → FeatureB takes b_param, passes: {c_param="C"}
    → FeatureC takes c_param, passes: {}
    → Base receives: {} (empty)
    """)

    # super() with arguments
    print("=== super() with Explicit Arguments ===")
    print("""
    # super() can take class and instance:
    super(FeatureA, self).__init__(**kwargs)

    # This is the same as:
    super().__init__(**kwargs)

    # In Python 3, super() automatically uses
    # the enclosing class and first argument.
    """)

main()



































# Using super() with Multiple Inheritance

class Base:
    def __init__(self, **kwargs):
        print(f"Base.__init__(kwargs={kwargs})")
        # End of chain - absorb remaining kwargs

    def process(self):
        print("Base.process()")
        return ["Base"]


class FeatureA(Base):
    def __init__(self, a_param="default_a", **kwargs):
        print(f"FeatureA.__init__(a_param={a_param}, kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass remaining kwargs up
        self.a_param = a_param

    def process(self):
        print("FeatureA.process()")
        result = super().process()  # Call next in chain
        return ["FeatureA"] + result


class FeatureB(Base):
    def __init__(self, b_param="default_b", **kwargs):
        print(f"FeatureB.__init__(b_param={b_param}, kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass remaining kwargs up
        self.b_param = b_param

    def process(self):
        print("FeatureB.process()")
        result = super().process()  # Call next in chain
        return ["FeatureB"] + result


class FeatureC(Base):
    def __init__(self, c_param="default_c", **kwargs):
        print(f"FeatureC.__init__(c_param={c_param}, kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass remaining kwargs up
        self.c_param = c_param

    def process(self):
        print("FeatureC.process()")
        result = super().process()  # Call next in chain
        return ["FeatureC"] + result


class Combined(FeatureA, FeatureB, FeatureC):
    def __init__(self, **kwargs):
        print(f"Combined.__init__(kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass all kwargs

    def process(self):
        print("Combined.process()")
        result = super().process()  # Start the chain
        return ["Combined"] + result


def main():
    print("=== super() Chain with **kwargs ===\n")

    # Show MRO
    print("MRO for Combined:")
    print([cls.__name__ for cls in Combined.__mro__])
    print()

    # Create with all parameters
    print("--- Creating Combined with all parameters ---")
    obj = 

    print(f"\nAttributes:")
    print(f"  obj.a_param = '{obj.a_param}'")
    print(f"  obj.b_param = '{obj.b_param}'")
    print(f"  obj.c_param = '{obj.c_param}'")

    # Process chain demonstration
    print("\n--- Calling process() ---")
    result = obj.process()
    print(f"\nResult: {result}")

    # Create with default parameters
    print("\n" + "=" * 50)
    print("--- Creating Combined with defaults ---")
    obj2 = Combined()

    print(f"\nAttributes (defaults):")
    print(f"  obj2.a_param = '{obj2.a_param}'")
    print(f"  obj2.b_param = '{obj2.b_param}'")
    print(f"  obj2.c_param = '{obj2.c_param}'")

    # Explain the pattern
    print("\n=== The **kwargs Pattern ===")
    print("""
    Each class:
    1. Accepts its own named parameter
    2. Accepts **kwargs for other classes
    3. Uses super().__init__(**kwargs)

    This lets each class extract its parameter
    and pass the rest up the chain!

    Example:
    Combined(a_param="A", b_param="B", c_param="C")

    → Combined receives: {a_param="A", b_param="B", c_param="C"}
    → FeatureA takes a_param, passes: {b_param="B", c_param="C"}
    → FeatureB takes b_param, passes: {c_param="C"}
    → FeatureC takes c_param, passes: {}
    → Base receives: {} (empty)
    """)

    # super() with arguments
    print("=== super() with Explicit Arguments ===")
    print("""
    # super() can take class and instance:
    super(FeatureA, self).__init__(**kwargs)

    # This is the same as:
    super().__init__(**kwargs)

    # In Python 3, super() automatically uses
    # the enclosing class and first argument.
    """)

main()



































# Using super() with Multiple Inheritance

class Base:
    def __init__(self, **kwargs):
        print(f"Base.__init__(kwargs={kwargs})")
        # End of chain - absorb remaining kwargs

    def process(self):
        print("Base.process()")
        return ["Base"]


class FeatureA(Base):
    def __init__(self, a_param="default_a", **kwargs):
        print(f"FeatureA.__init__(a_param={a_param}, kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass remaining kwargs up
        self.a_param = a_param

    def process(self):
        print("FeatureA.process()")
        result = super().process()  # Call next in chain
        return ["FeatureA"] + result


class FeatureB(Base):
    def __init__(self, b_param="default_b", **kwargs):
        print(f"FeatureB.__init__(b_param={b_param}, kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass remaining kwargs up
        self.b_param = b_param

    def process(self):
        print("FeatureB.process()")
        result = super().process()  # Call next in chain
        return ["FeatureB"] + result


class FeatureC(Base):
    def __init__(self, c_param="default_c", **kwargs):
        print(f"FeatureC.__init__(c_param={c_param}, kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass remaining kwargs up
        self.c_param = c_param

    def process(self):
        print("FeatureC.process()")
        result = super().process()  # Call next in chain
        return ["FeatureC"] + result


class Combined(FeatureA, FeatureB, FeatureC):
    def __init__(self, **kwargs):
        print(f"Combined.__init__(kwargs={kwargs})")
        super().__init__(**kwargs)  # Pass all kwargs

    def process(self):
        print("Combined.process()")
        result = super().process()  # Start the chain
        return ["Combined"] + result


def main():
    print("=== super() Chain with **kwargs ===\n")

    # Show MRO
    print("MRO for Combined:")
    print([cls.__name__ for cls in Combined.__mro__])
    print()

    # Create with all parameters
    print("--- Creating Combined with all parameters ---")
    obj = 

    print(f"\nAttributes:")
    print(f"  obj.a_param = '{obj.a_param}'")
    print(f"  obj.b_param = '{obj.b_param}'")
    print(f"  obj.c_param = '{obj.c_param}'")

    # Process chain demonstration
    print("\n--- Calling process() ---")
    result = obj.process()
    print(f"\nResult: {result}")

    # Create with default parameters
    print("\n" + "=" * 50)
    print("--- Creating Combined with defaults ---")
    obj2 = Combined()

    print(f"\nAttributes (defaults):")
    print(f"  obj2.a_param = '{obj2.a_param}'")
    print(f"  obj2.b_param = '{obj2.b_param}'")
    print(f"  obj2.c_param = '{obj2.c_param}'")

    # Explain the pattern
    print("\n=== The **kwargs Pattern ===")
    print("""
    Each class:
    1. Accepts its own named parameter
    2. Accepts **kwargs for other classes
    3. Uses super().__init__(**kwargs)

    This lets each class extract its parameter
    and pass the rest up the chain!

    Example:
    Combined(a_param="A", b_param="B", c_param="C")

    → Combined receives: {a_param="A", b_param="B", c_param="C"}
    → FeatureA takes a_param, passes: {b_param="B", c_param="C"}
    → FeatureB takes b_param, passes: {c_param="C"}
    → FeatureC takes c_param, passes: {}
    → Base receives: {} (empty)
    """)

    # super() with arguments
    print("=== super() with Explicit Arguments ===")
    print("""
    # super() can take class and instance:
    super(FeatureA, self).__init__(**kwargs)

    # This is the same as:
    super().__init__(**kwargs)

    # In Python 3, super() automatically uses
    # the enclosing class and first argument.
    """)

main()



































super() follows MRO, not direct parent. Essential for diamond pattern.

Conflict resolution

What happens when parents have same method.

conflict_resolution.py
# Handling Method Name Conflicts

class Logger:
    def log(self, message):
        return f"[LOG] {message}"

    def get_info(self):
        return "Logger: Provides logging capability"


class Serializer:
    def serialize(self):
        return f"<{self.__class__.__name__}/>"

    def get_info(self):
        return "Serializer: Provides serialization"


class Validator:
    def validate(self, data):
        return len(data) > 0

    def get_info(self):
        return "Validator: Provides validation"


# Method name conflict: all have get_info()
class DataProcessor(Logger, Serializer, Validator):
    def process(self, data):
        if self.validate(data):
            self.log(f"Processing: {data}")
            return self.serialize()
        return None


# Solution 1: Override and choose
class ProcessorV1(Logger, Serializer, Validator):
    def get_info(self):
        # Explicitly choose Logger's version
        return Logger.get_info(self)


# Solution 2: Override and combine
class ProcessorV2(Logger, Serializer, Validator):
    def get_info(self):
        # Combine all parent info
        info_parts = [
            Logger.get_info(self),
            Serializer.get_info(self),
            Validator.get_info(self),
        ]
        return " | ".join(info_parts)


# Solution 3: Use super() chain
class LoggerChain:
    def get_info(self):
        info = "Logger: Provides logging"
        parent_info = super().get_info() if hasattr(super(), 'get_info') else ""
        return info + (" | " + parent_info if parent_info else "")


class SerializerChain:
    def get_info(self):
        info = "Serializer: Provides serialization"
        parent_info = super().get_info() if hasattr(super(), 'get_info') else ""
        return info + (" | " + parent_info if parent_info else "")


class ValidatorChain:
    def get_info(self):
        info = "Validator: Provides validation"
        parent_info = super().get_info() if hasattr(super(), 'get_info') else ""
        return info + (" | " + parent_info if parent_info else "")


class Base:
    def get_info(self):
        return ""  # End of chain


class ProcessorV3(LoggerChain, SerializerChain, ValidatorChain, Base):
    pass


def main():
    print("=== Handling Method Name Conflicts ===\n")

    # Show the conflict
    print("--- The Conflict ---")
    print("Logger, Serializer, and Validator all have get_info()")
    print("When a class inherits from all three, which one wins?\n")

    # Default behavior: leftmost wins
    print("--- Default Behavior (Leftmost Wins) ---")
    proc = DataProcessor()
    print(f"MRO: {[c.__name__ for c in DataProcessor.__mro__]}")
    print(f"proc.get_info() = '{proc.get_info()}'")
    print("  → Logger's version (first in inheritance list)")

    # Solution 1: Explicitly choose
    print("\n--- Solution 1: Explicitly Choose ---")
    v1 = ProcessorV1()
    print(f"v1.get_info() = '{v1.get_info()}'")
    print("  → Override calls Logger.get_info(self) directly")

    # Solution 2: Combine all
    print("\n--- Solution 2: Combine All ---")
    v2 = ProcessorV2()
    print(f"v2.get_info() = '{v2.get_info()}'")
    print("  → Override calls all three parent methods")

    # Solution 3: super() chain
    print("\n--- Solution 3: super() Chain ---")
    v3 = ProcessorV3()
    print(f"MRO: {[c.__name__ for c in ProcessorV3.__mro__]}")
    print(f"v3.get_info() = '{v3.get_info()}'")
    print("  → Each class calls super().get_info() to chain")

    # Accessing specific parent methods
    print("\n--- Accessing Specific Parent Methods ---")
    print(f"Logger.get_info(proc) = '{Logger.get_info(proc)}'")
    print(f"Serializer.get_info(proc) = '{Serializer.get_info(proc)}'")
    print(f"Validator.get_info(proc) = '{Validator.get_info(proc)}'")
    print("  → Can always call parent method directly with instance")

    # Best practices
    print("\n=== Best Practices for Conflicts ===")
    print("""
    1. Avoid conflicts by using unique method names
    2. If conflict unavoidable, override in child
    3. Use ParentClass.method(self) for explicit calls
    4. Design parent classes to work with super() chain
    5. Document which parent's method is used
    6. Consider composition over inheritance
    """)

main()






















































Leftmost parent wins. Explicit override if you need different behavior.

Exercise: practical.py

Build a game character with multiple capability classes