Skip to main content

Python MRO — Method Resolution Order: Practice Problems & Exercises

Practice: MRO — Method Resolution Order and the C3 Linearisation Algorithm

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1Read the MROEasy
MRO__mro__

Print the Method Resolution Order of class D as a list of class names (strings), using the __mro__ attribute.

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

print([c.__name__ for c in D.__mro__])
Solution
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

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

# Alternative using mro() method:
print([c.__name__ for c in D.mro()])

Explanation: Python's C3 linearisation for D(B, C) where both B and C inherit from A produces D -> B -> C -> A -> object. This ensures A appears only once (after all its subclasses) and preserves the left-to-right order of the base classes listed in D's definition.

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

# TODO: print the MRO of D as a list of class names
print([])
Expected Output
['D', 'B', 'C', 'A', 'object']
Hints

Hint 1: Use D.__mro__ — it returns a tuple of classes.

Hint 2: Extract the __name__ attribute from each class in the tuple.


#2Predict Method ResolutionEasy
MROmethod-resolution

Without changing the class definitions, predict what d.hello() returns. Then verify by running the code.

Python
class A:
    def hello(self):
        return "A"

class B(A):
    def hello(self):
        return "B"

class C(A):
    def hello(self):
        return "C"

class D(B, C):
    pass

d = D()
print(d.hello())
Solution
class A:
def hello(self):
return "A"

class B(A):
def hello(self):
return "B"

class C(A):
def hello(self):
return "C"

class D(B, C):
pass

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

Explanation: Python walks the MRO in order: D (no hello), B (has hello) — found, stops. Because B appears before C in D(B, C), B.hello wins. This is deterministic and consistent regardless of instance state.

class A:
  def hello(self):
      return "A"

class B(A):
  def hello(self):
      return "B"

class C(A):
  def hello(self):
      return "C"

class D(B, C):
  pass

d = D()
# TODO: what does d.hello() return? Print it.
print(d.hello())
Expected Output
B
Hints

Hint 1: Python walks the MRO left-to-right and stops at the first class that defines the method.

Hint 2: D inherits from B before C, so B.hello is found first.


#3super() Calls the Next in MROEasy
superMROcooperative-inheritance

Implement Child.greet() using super() so that it prepends "Child -> " to the result returned by the parent.

Python
class Base:
    def greet(self):
        return "Base"

class Child(Base):
    def greet(self):
        return "Child -> " + super().greet()

c = Child()
print(c.greet())
Solution
class Base:
def greet(self) -> str:
return "Base"

class Child(Base):
def greet(self) -> str:
parent_result = super().greet() # walks MRO to Base.greet
return f"Child -> {parent_result}"

c = Child()
print(c.greet()) # Child -> Base

Explanation: super() does not simply mean "call the parent class". It returns a proxy that resolves the next class in the MRO of the object's concrete type. For a simple single-inheritance chain this is the same as the parent, but in multiple-inheritance scenarios it becomes the key mechanism for cooperative method chaining.

class Base:
  def greet(self):
      return "Base"

class Child(Base):
  def greet(self):
      # TODO: use super() to call Base.greet() and prepend "Child -> "
      pass

c = Child()
print(c.greet())
Expected Output
Child -> Base
Hints

Hint 1: super().greet() calls the next class in the MRO.

Hint 2: Return a string that combines "Child -> " with the result of super().greet().


#4Identify an Invalid MROEasy
MROTypeErrorC3

Run the code and observe the TypeError. In your own words (as a comment), explain why Python cannot create a consistent MRO for class Bad(A, B).

Python
class A: pass
class B(A): pass

try:
    class Bad(A, B): pass
except TypeError as e:
    print(f"TypeError: {e}")
Solution
class A: pass
class B(A): pass

# Bad(A, B) says: resolve A before B.
# But B is a subclass of A, so C3 requires A to come AFTER B.
# These two constraints are contradictory → TypeError.

try:
class Bad(A, B): pass
except TypeError as e:
print(f"TypeError: {e}")

# Valid ordering (B inherits A, so B must come first):
class Good(B, A): pass
print([c.__name__ for c in Good.__mro__]) # ['Good', 'B', 'A', 'object']

Explanation: C3 linearisation enforces the monotonicity constraint: a class must always appear after all its subclasses. Since B is a subclass of A, A must come after B. Writing Bad(A, B) demands the opposite order, which is an irreconcilable contradiction and Python raises TypeError at class definition time.

class A: pass
class B(A): pass

# This class definition raises TypeError.
# Run it and read the error, then answer in a comment:
# Why does Python reject this MRO?

try:
  class Bad(A, B): pass
except TypeError as e:
  print(f"TypeError: {e}")
Expected Output
TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B
Hints

Hint 1: C3 linearisation requires that if B inherits A, then A must come after B in every linearisation.

Hint 2: Bad(A, B) demands A before B, but B is a subclass of A — contradiction.


Medium

#5Cooperative super() ChainMedium
supercooperative-inheritanceMRO

Trace the cooperative super() chain and predict the output before running. Each class prepends its own name to the result of super().process(). The MRO of D is D -> B -> C -> A -> object.

Python
class A:
    def process(self):
        return ["A"]

class B(A):
    def process(self):
        return ["B"] + super().process()

class C(A):
    def process(self):
        return ["C"] + super().process()

class D(B, C):
    def process(self):
        return ["D"] + super().process()

d = D()
print(d.process())
Solution
class A:
def process(self):
return ["A"]

class B(A):
def process(self):
return ["B"] + super().process()

class C(A):
def process(self):
return ["C"] + super().process()

class D(B, C):
def process(self):
return ["D"] + super().process()

d = D()
print(d.process())
# ['D', 'B', 'C', 'A']

# Trace (MRO: D -> B -> C -> A):
# D.process() -> ["D"] + B.process(via super)
# B.process() -> ["B"] + C.process(via super, NOT A!)
# C.process() -> ["C"] + A.process(via super)
# A.process() -> ["A"]

Explanation: This is the critical insight about super(): when called from B.process in the context of a D instance, super() does not jump to A (B's parent). Instead it advances to the next step in D's MRO, which is C. This is why super() enables cooperative multiple inheritance — every class in the chain gets exactly one call.

class A:
  def process(self):
      return ["A"]

class B(A):
  def process(self):
      return ["B"] + super().process()

class C(A):
  def process(self):
      return ["C"] + super().process()

class D(B, C):
  def process(self):
      return ["D"] + super().process()

d = D()
print(d.process())
Expected Output
['D', 'B', 'C', 'A']
Hints

Hint 1: Trace the MRO: D -> B -> C -> A -> object.

Hint 2: super() in B does NOT call A directly — it calls the next class in D's MRO, which is C.


#6Diamond Problem — Ensure Single InitialisationMedium
diamond-problemsupercooperative-inheritance

Run the diamond hierarchy and verify that Resource.__init__ is called exactly once, even though both Reader and Writer inherit from it. Cooperative super() is already in place — trace the execution order.

Python
class Resource:
    def __init__(self):
        print("Resource.__init__")
        self.value = 42

class Reader(Resource):
    def __init__(self):
        super().__init__()
        print("Reader.__init__")

class Writer(Resource):
    def __init__(self):
        super().__init__()
        print("Writer.__init__")

class ReadWriter(Reader, Writer):
    def __init__(self):
        super().__init__()
        print("ReadWriter.__init__")

rw = ReadWriter()
print(rw.value)
Solution
class Resource:
def __init__(self) -> None:
print("Resource.__init__")
self.value = 42

class Reader(Resource):
def __init__(self) -> None:
super().__init__() # calls Writer.__init__ (next in MRO)
print("Reader.__init__")

class Writer(Resource):
def __init__(self) -> None:
super().__init__() # calls Resource.__init__ (next in MRO)
print("Writer.__init__")

class ReadWriter(Reader, Writer):
def __init__(self) -> None:
super().__init__() # calls Reader.__init__ (next in MRO)
print("ReadWriter.__init__")

# MRO: ReadWriter -> Reader -> Writer -> Resource -> object
print([c.__name__ for c in ReadWriter.__mro__])

rw = ReadWriter()
# Output order (prints happen AFTER each super() returns):
# Resource.__init__
# Writer.__init__
# Reader.__init__
# ReadWriter.__init__
print(rw.value) # 42

Explanation: The MRO ReadWriter -> Reader -> Writer -> Resource guarantees Resource.__init__ runs once. Without cooperative super(), a naive Resource.__init__() call in both Reader and Writer would initialise Resource twice. The cooperative chain threads through the MRO visiting each class exactly once.

class Resource:
  def __init__(self):
      print("Resource.__init__")
      self.value = 42

class Reader(Resource):
  def __init__(self):
      super().__init__()
      print("Reader.__init__")

class Writer(Resource):
  def __init__(self):
      super().__init__()
      print("Writer.__init__")

class ReadWriter(Reader, Writer):
  def __init__(self):
      super().__init__()
      print("ReadWriter.__init__")

rw = ReadWriter()
print(rw.value)
Expected Output
Resource.__init__\nReader.__init__\nWriter.__init__\nReadWriter.__init__\n42
Hints

Hint 1: Cooperative super() ensures Resource.__init__ is called exactly once.

Hint 2: Trace the MRO: ReadWriter -> Reader -> Writer -> Resource -> object.


#7Mixin Order MattersMedium
mixinMROordering

Demonstrate that the order in which mixins are listed changes the output. Order1 lists TimestampMixin first; Order2 lists PrefixMixin first. Predict then verify both outputs.

Python
class TimestampMixin:
    def describe(self):
        return f"[ts] {super().describe()}"

class PrefixMixin:
    def describe(self):
        return f"[px] {super().describe()}"

class Base:
    def describe(self):
        return "base"

class Order1(TimestampMixin, PrefixMixin, Base): pass
class Order2(PrefixMixin, TimestampMixin, Base): pass

print(Order1().describe())   # [ts] [px] base
print(Order2().describe())   # [px] [ts] base
Solution
class TimestampMixin:
def describe(self) -> str:
return f"[ts] {super().describe()}"

class PrefixMixin:
def describe(self) -> str:
return f"[px] {super().describe()}"

class Base:
def describe(self) -> str:
return "base"

class Order1(TimestampMixin, PrefixMixin, Base): pass
class Order2(PrefixMixin, TimestampMixin, Base): pass

print(Order1().describe())
# MRO: Order1 -> TimestampMixin -> PrefixMixin -> Base
# [ts] [px] base

print(Order2().describe())
# MRO: Order2 -> PrefixMixin -> TimestampMixin -> Base
# [px] [ts] base

print([c.__name__ for c in Order1.__mro__])
print([c.__name__ for c in Order2.__mro__])

Explanation: The leftmost class in the inheritance list is the outermost wrapper because Python's MRO places it first. When Order1().describe() runs, TimestampMixin.describe is called first, which calls super().describe() forwarding to PrefixMixin, which finally reaches Base. Mixin ordering is therefore a design decision with observable consequences.

class TimestampMixin:
  def describe(self):
      return f"[ts] {super().describe()}"

class PrefixMixin:
  def describe(self):
      return f"[px] {super().describe()}"

class Base:
  def describe(self):
      return "base"

# TODO: create two classes:
# Order1 applies TimestampMixin then PrefixMixin
# Order2 applies PrefixMixin then TimestampMixin
# Print both results

class Order1(TimestampMixin, PrefixMixin, Base): pass
class Order2(PrefixMixin, TimestampMixin, Base): pass

print(Order1().describe())
print(Order2().describe())
Expected Output
[ts] [px] base\n[px] [ts] base
Hints

Hint 1: The leftmost mixin in the class definition is applied outermost (called first).

Hint 2: MRO for Order1: Order1 -> TimestampMixin -> PrefixMixin -> Base.


#8Compute the MRO by Hand (C3)Medium
C3-linearisationMROalgorithm

Apply the C3 linearisation algorithm by hand to compute the MRO of E(D, C). Write your prediction then verify it matches Python's output.

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

predicted = ["E", "D", "B", "C", "A", "object"]
actual = [c.__name__ for c in E.__mro__]

print("Predicted:", predicted)
print("Actual:   ", actual)
print("Match:", predicted == actual)
Solution
# C3 manual derivation:
# L(A) = [A, object]
# L(B) = [B, A, object]
# L(C) = [C, A, object]
# L(D) = D + merge([B,A,object], [C,A,object], [B,C])
# = [D, B, C, A, object]
# L(E) = E + merge([D,B,C,A,object], [C,A,object], [D,C])
# Step 1: take D (head of first list, not in tail of others) → E,D
# Step 2: take B → E,D,B
# Step 3: take C → E,D,B,C
# Step 4: take A → E,D,B,C,A
# Step 5: take object → E,D,B,C,A,object

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

predicted = ["E", "D", "B", "C", "A", "object"]
actual = [c.__name__ for c in E.__mro__]

print("Predicted:", predicted)
print("Actual: ", actual)
print("Match:", predicted == actual) # True

Explanation: C3 takes the head of each linearisation list only if it does not appear in the tail of any other list. This produces the unique, consistent, monotonic ordering that respects both local precedence (order of bases listed) and the inheritance graph constraints.

# Given this hierarchy, manually compute the MRO of E
# and verify with Python.

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

# TODO: write your predicted MRO as a list of names,
# then verify it matches Python's result.
predicted = []  # e.g. ["E", "D", "B", "C", "A", "object"]

actual = [c.__name__ for c in E.__mro__]
print("Predicted:", predicted)
print("Actual:   ", actual)
print("Match:", predicted == actual)
Expected Output
Predicted: ['E', 'D', 'B', 'C', 'A', 'object']\nActual:    ['E', 'D', 'B', 'C', 'A', 'object']\nMatch: True
Hints

Hint 1: C3: merge the linearisations of the bases left-to-right, plus the base list itself.

Hint 2: L(E) = E + merge(L(D), L(C), [D, C]). L(D) = [D, B, C, A, object].


Hard

#9Fix a Broken super() ChainHard
superMROdebugging

AuthPlugin uses a hardcoded Plugin.setup(self) call which breaks the cooperative chain and skips CachePlugin.setup. Fix it using super().

Python
class Plugin:
    def setup(self):
        print("Plugin.setup")

class AuthPlugin(Plugin):
    def setup(self):
        super().setup()   # fixed: use super() not Plugin.setup(self)
        print("AuthPlugin.setup")

class CachePlugin(Plugin):
    def setup(self):
        super().setup()
        print("CachePlugin.setup")

class App(AuthPlugin, CachePlugin):
    def setup(self):
        super().setup()
        print("App.setup")

app = App()
app.setup()
Solution
class Plugin:
def setup(self) -> None:
print("Plugin.setup")

class AuthPlugin(Plugin):
def setup(self) -> None:
super().setup() # KEY FIX: super() walks MRO, not hardcoded parent
print("AuthPlugin.setup")

class CachePlugin(Plugin):
def setup(self) -> None:
super().setup()
print("CachePlugin.setup")

class App(AuthPlugin, CachePlugin):
def setup(self) -> None:
super().setup()
print("App.setup")

# MRO: App -> AuthPlugin -> CachePlugin -> Plugin -> object
print([c.__name__ for c in App.__mro__])

app = App()
app.setup()
# Plugin.setup
# CachePlugin.setup
# AuthPlugin.setup
# App.setup

Explanation: Hardcoding Plugin.setup(self) in AuthPlugin bypasses the MRO completely and calls Plugin.setup directly, skipping CachePlugin in the chain. super().setup() instead advances to the next class in the concrete instance's MRO — which in an App context is CachePlugin. This is why cooperative super() is required whenever a method might be mixed in with others.

class Plugin:
  def setup(self):
      print("Plugin.setup")

class AuthPlugin(Plugin):
  def setup(self):
      Plugin.setup(self)   # BUG: hardcoded parent call
      print("AuthPlugin.setup")

class CachePlugin(Plugin):
  def setup(self):
      super().setup()
      print("CachePlugin.setup")

class App(AuthPlugin, CachePlugin):
  def setup(self):
      super().setup()
      print("App.setup")

app = App()
app.setup()
# Current output skips CachePlugin.setup.
# Fix AuthPlugin so CachePlugin.setup is also called.
Expected Output
Plugin.setup\nCachePlugin.setup\nAuthPlugin.setup\nApp.setup
Hints

Hint 1: Replace the hardcoded Plugin.setup(self) call with super().setup().

Hint 2: super() in AuthPlugin resolves to CachePlugin when called on an App instance.


#10Mixin with __init__ Parameter ForwardingHard
mixinsuperkwargscooperative-inheritance

Instantiate TaggedDocument with title and tags keyword arguments. Each mixin consumes its own parameter and forwards the rest through **kwargs. All three attributes should be set correctly.

Python
class TimestampMixin:
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.created_at = "2026-03-21"

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

class Document:
    def __init__(self, title: str, **kwargs):
        super().__init__(**kwargs)
        self.title = title

class TaggedDocument(TimestampMixin, TaggedMixin, Document):
    pass

doc = TaggedDocument(title="Python OOP", tags=["python", "oop"])
print(doc.title)
print(doc.tags)
print(doc.created_at)
Solution
class TimestampMixin:
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.created_at = "2026-03-21"

class TaggedMixin:
def __init__(self, tags=None, **kwargs) -> None:
super().__init__(**kwargs) # passes title onwards
self.tags = tags or []

class Document:
def __init__(self, title: str, **kwargs) -> None:
super().__init__(**kwargs) # passes {} to object.__init__
self.title = title

class TaggedDocument(TimestampMixin, TaggedMixin, Document):
pass

# MRO: TaggedDocument -> TimestampMixin -> TaggedMixin -> Document -> object
# kwargs flow:
# TaggedDocument(**{'title':'Python OOP','tags':[...]})
# -> TimestampMixin(**{'title':'...','tags':[...]})
# -> TaggedMixin(tags=[...], **{'title':'...'}) # consumes tags
# -> Document(title='...', **{}) # consumes title
# -> object.__init__()

doc = TaggedDocument(title="Python OOP", tags=["python", "oop"])
print(doc.title) # Python OOP
print(doc.tags) # ['python', 'oop']
print(doc.created_at) # 2026-03-21

Explanation: The **kwargs forwarding pattern is the standard technique for writing mixins that are safe in multiple-inheritance chains. Each class in the MRO extracts the keyword arguments it needs and passes the remainder downstream. object.__init__ is called last and accepts an empty dict. Without **kwargs, any mixin that accepted extra parameters would cause a TypeError.

class TimestampMixin:
  def __init__(self, **kwargs):
      super().__init__(**kwargs)
      self.created_at = "2026-03-21"

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

class Document:
  def __init__(self, title: str, **kwargs):
      super().__init__(**kwargs)
      self.title = title

class TaggedDocument(TimestampMixin, TaggedMixin, Document):
  pass

# TODO: instantiate TaggedDocument with title and tags
# then print all three attributes

doc = TaggedDocument(title="Python OOP", tags=["python", "oop"])
print(doc.title)
print(doc.tags)
print(doc.created_at)
Expected Output
Python OOP\n['python', 'oop']\n2026-03-21
Hints

Hint 1: Each __init__ consumes its own keyword argument and passes the rest through **kwargs.

Hint 2: The MRO chain threads kwargs through until object.__init__ receives an empty dict.


#11Build a Hook System Using MROHard
MROsuperdesign-patternshooks

Compose three hooks into FullPipeline using MRO. ValidationHook runs first, LoggingHook logs the result after transformation, and UpperHook transforms the string. All hooks use super().run() to pass data down the chain.

Python
class Pipeline:
    def run(self, data: str) -> str:
        return data

class ValidationHook(Pipeline):
    def run(self, data: str) -> str:
        if not data:
            raise ValueError("Empty input")
        return super().run(data)

class LoggingHook(Pipeline):
    def run(self, data: str) -> str:
        result = super().run(data)
        print(f"LOG: '{data}' -> '{result}'")
        return result

class UpperHook(Pipeline):
    def run(self, data: str) -> str:
        result = super().run(data)
        return result.upper()

class FullPipeline(ValidationHook, LoggingHook, UpperHook, Pipeline):
    pass

fp = FullPipeline()
print(fp.run("hello"))
Solution
class Pipeline:
def run(self, data: str) -> str:
return data # identity transform at the base

class ValidationHook(Pipeline):
def run(self, data: str) -> str:
if not data:
raise ValueError("Empty input")
return super().run(data) # passes data to LoggingHook

class LoggingHook(Pipeline):
def run(self, data: str) -> str:
result = super().run(data) # passes data to UpperHook first
print(f"LOG: '{data}' -> '{result}'") # logs after transformation
return result

class UpperHook(Pipeline):
def run(self, data: str) -> str:
result = super().run(data) # gets "hello" from Pipeline base
return result.upper() # returns "HELLO" back up the chain

# MRO: FullPipeline -> ValidationHook -> LoggingHook -> UpperHook -> Pipeline
class FullPipeline(ValidationHook, LoggingHook, UpperHook, Pipeline):
pass

print([c.__name__ for c in FullPipeline.__mro__])
fp = FullPipeline()
print(fp.run("hello"))
# LOG: 'hello' -> 'HELLO'
# HELLO

# Verify validation:
try:
fp.run("")
except ValueError as e:
print(f"Caught: {e}") # Caught: Empty input

Explanation: The super() chain enables each hook to wrap the call to the next. Because LoggingHook calls super().run(data) before printing, the log captures the already-uppercased result. Reordering the hooks in the class definition changes both the transformation and the logged output — MRO order is therefore a critical design decision for hook systems.

class Pipeline:
  def run(self, data: str) -> str:
      return data

class ValidationHook(Pipeline):
  def run(self, data: str) -> str:
      if not data:
          raise ValueError("Empty input")
      return super().run(data)

class LoggingHook(Pipeline):
  def run(self, data: str) -> str:
      result = super().run(data)
      print(f"LOG: '{data}' -> '{result}'")
      return result

class UpperHook(Pipeline):
  def run(self, data: str) -> str:
      result = super().run(data)
      return result.upper()

# TODO: compose all three hooks in the correct order:
# ValidationHook should run first,
# then LoggingHook (logs the final result),
# then UpperHook transforms the data.

class FullPipeline(ValidationHook, LoggingHook, UpperHook, Pipeline):
  pass

fp = FullPipeline()
print(fp.run("hello"))
Expected Output
LOG: 'hello' -> 'HELLO'\nHELLO
Hints

Hint 1: MRO for FullPipeline: FullPipeline -> ValidationHook -> LoggingHook -> UpperHook -> Pipeline.

Hint 2: super() chains all hooks — ValidationHook runs first, then LoggingHook, then UpperHook, then Pipeline.

© 2026 EngineersOfAI. All rights reserved.