Python MRO — Method Resolution Order: Practice Problems & Exercises
Practice: MRO — Method Resolution Order and the C3 Linearisation Algorithm
← Back to lessonEasy
Print the Method Resolution Order of class D as a list of class names (strings), using the __mro__ attribute.
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.
Without changing the class definitions, predict what d.hello() returns. Then verify by running the code.
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
BHints
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.
Implement Child.greet() using super() so that it prepends "Child -> " to the result returned by the parent.
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 -> BaseHints
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().
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).
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, BHints
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
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.
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.
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.
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__\n42Hints
Hint 1: Cooperative super() ensures Resource.__init__ is called exactly once.
Hint 2: Trace the MRO: ReadWriter -> Reader -> Writer -> Resource -> object.
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.
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] baseSolution
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] baseHints
Hint 1: The leftmost mixin in the class definition is applied outermost (called first).
Hint 2: MRO for Order1: Order1 -> TimestampMixin -> PrefixMixin -> Base.
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.
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: TrueHints
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
AuthPlugin uses a hardcoded Plugin.setup(self) call which breaks the cooperative chain and skips CachePlugin.setup. Fix it using super().
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.setupHints
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.
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.
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-21Hints
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.
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.
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'\nHELLOHints
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.
