Articles
Python MRO Interview Questions: super(), Mixins, and Cooperative Initialization
Practice Python MRO interview questions with diamond traces, cooperative super(), mixin ordering, keyword ownership, and executable initialization tests.
In a Python MRO interview, explain super() as a search starting after the current class in the instance's method resolution order. It does not mean “call my declared parent.” The distinction becomes visible when a diamond hierarchy inserts a sibling between a mixin and its shared base. A correct answer predicts the lookup, traces actual calls, and proves that initialization happens once.
This article uses official Python documentation for language behavior and original exercises for practice. It does not claim that a particular employer asks these examples.
What exactly does super() do?
super(Current, instance) searches the runtime class's MRO after Current. In an ordinary instance method, zero-argument super() gets the defining class and first argument from the method's context. A sibling can therefore be the next destination. The official built-in reference describes this as delegation through a proxy. Python super documentation
Consider a review comment saying, “Replace Base.save(self) with super().save(); they are equivalent.” That is too broad. The explicit call names one implementation. The cooperative call lets the composition determine which implementation comes next. The change can deliberately include a mixin that the explicit call skipped.
In an interview, identify three separate objects: the class that defined the executing method, the runtime class of self, and the class that supplies the next matching method. Confusing those produces most incorrect traces. Also distinguish attribute lookup from execution: an MRO lists possible search destinations, but a method that never delegates ends the call chain.
Do not replace the defining class with type(self) in an explicit super call. Inside an inherited method, doing that can restart lookup at the wrong point and re-enter the same method. Zero-argument super() expresses the usual intent more clearly.
Can you trace a diamond without guessing?
Run this original exercise before discussing constructor arguments:
class Root:
def visit(self, log):
log.append("Root")
class Left(Root):
def visit(self, log):
log.append("Left")
super().visit(log)
class Right(Root):
def visit(self, log):
log.append("Right")
super().visit(log)
class Combined(Left, Right):
def visit(self, log):
log.append("Combined")
super().visit(log)
log = []
Combined().visit(log)
assert log == ["Combined", "Left", "Right", "Root"]
assert [c.__name__ for c in Combined.__mro__] == [
"Combined", "Left", "Right", "Root", "object"
]
When Left.visit executes, self is still a Combined. Its super() lookup reaches Right.visit, even though Right is not a declared base of Left. This is the crucial sentence to say aloud. Root.visit is reached once because both intermediate methods cooperate and the final implementation deliberately ends this particular protocol.
Now swap the bases to Combined(Right, Left). Predict the new trace before running it. Next, remove delegation from Left.visit. The MRO remains available for inspection, but execution stops at Left; the remaining methods are not scheduled automatically.
Finally, move each append after its super() call. The entry order stays the same while the visible log reverses as calls return. This variation tests whether you understand ordinary call-stack behavior as well as method selection. Avoid memorizing one printed sequence as if it were a universal diamond rule.
How does C3 choose an order?
Python uses C3 linearization for ordinary class hierarchies. It preserves local base ordering and monotonicity: a subclass cannot reverse an ordering imposed by its ancestors. The historical MRO HOWTO explains the merge procedure and inconsistent hierarchies. Its Python 2.3 title describes the algorithm's introduction, not an instruction to use an obsolete interpreter. Python MRO HOWTO
For the example, merge the parent linearizations [Left, Root, object] and [Right, Root, object] with the direct-base list [Left, Right]. Select a head only if it does not appear in another list's tail. After choosing Left, Root cannot yet be selected because it appears behind Right. Choose Right, then Root, then object.
A useful follow-up is to define A(X, Y) and B(Y, X), then attempt C(A, B). One side requires X before Y; the other requires the reverse. Python rejects the inconsistent class definition with TypeError. It does not pick whichever branch was visited first.
For most interviews, a short merge demonstration is enough. Spend the remaining time connecting order to observable behavior. An elaborate hand-derived algorithm is less useful than noticing that an authorization mixin now executes after a method with a side effect.
Why do direct parent calls break cooperation?
Change the diamond so that Left.visit and Right.visit each invoke Root.visit(self, log) directly. If Combined.visit then explicitly calls both branches, the shared root runs twice. In a toy trace that means a repeated label. In an initialization path it might mean resetting state after another mixin configured it.
The defect is not that explicit calls are forbidden. They are appropriate when selecting a particular implementation is the intended contract. The defect is mixing fixed destinations with a design that assumes every participant advances through one shared chain.
Write the expected behavior first: should every extension run, should only one implementation win, or should two independent collaborators be called? Those are different designs. Using super() everywhere does not make an incoherent contract correct.
An interview repair should include a counterexample. Add a new Metrics mixin between the existing branches and demonstrate that the cooperative version includes it without editing older mixins. Then show which explicit call bypasses it. This provides evidence for the design benefit without claiming that multiple inheritance is always preferable.
How should cooperative constructors consume arguments?
The constructor protocol needs an agreement about parameter ownership. In this original design, each participant consumes its own keyword-only parameter, forwards the rest, and delegates once. The endpoint rejects leftovers instead of silently hiding a misspelled option.
class InitEnd:
def __init__(self, **kwargs):
if kwargs:
raise TypeError(f"unexpected options: {sorted(kwargs)}")
super().__init__()
class Named(InitEnd):
def __init__(self, *, name, **kwargs):
self.name = name
super().__init__(**kwargs)
class Tagged(InitEnd):
def __init__(self, *, tags=(), **kwargs):
self.tags = tuple(tags)
super().__init__(**kwargs)
class Job(Named, Tagged):
pass
job = Job(name="daily-export", tags=["billing"])
assert job.name == "daily-export"
assert job.tags == ("billing",)
assert [c.__name__ for c in Job.__mro__] == [
"Job", "Named", "Tagged", "InitEnd", "object"
]
Trace the payload as carefully as the method names. Named removes name; Tagged removes tags; InitEnd receives no remaining options. The shared endpoint is reached once. Python's data model distinguishes object creation from initialization; this example coordinates __init__, not a custom allocation protocol. Python object initialization
Now try Job(name="daily-export", tagz=["billing"]). The endpoint should reject tagz. A permissive endpoint that discards unknown keywords makes the object look valid while omitting the requested behavior. Test the missing required name separately; that failure belongs to Named's signature.
This constructor is an interview-sized protocol, not a universal framework recipe. If a third-party base has a fixed noncooperative signature, forwarding arbitrary keywords to it is not a repair. Introduce an adapter with explicit ownership, or compose the third-party object instead.
What ordering assumptions make a mixin fragile?
Suppose Tagged.__init__ reads self.name before delegating. It now depends on Named having executed first. Reordering the bases may preserve a valid MRO yet break the object's semantics. C3 guarantees a consistent search order, not that every class permutation is meaningful.
Choose and document whether a mixin performs work before or after delegation. A mixin that needs fully initialized downstream state might delegate first. A mixin that prepares inputs for downstream initialization might act first. Neither policy removes the need to state invariants.
Also ask what happens on failure. If one initializer opens a file and the next raises, a later line in the first initializer will not run normally. Avoid acquiring unrelated external resources inside a long constructor chain without a cleanup design. A separate factory or context-managed setup phase can make ownership easier to review.
Keep each mixin's responsibility narrow. If it assumes a database connection, an event loop, a particular attribute naming convention, and a precise neighbor, composition may expose those dependencies more honestly. That recommendation is engineering judgment derived from the exercise, not a language restriction.
Which tests demonstrate that the repair works?
Test effects, not just the textual MRO. The following matrix extends the Job exercise and makes the contract reviewable.
| Change | Expected evidence | Defect exposed |
|---|---|---|
| Construct with name and tags | Both fields contain supplied values | Skipped participant |
| Add an unknown keyword | Construction raises TypeError | Silently discarded configuration |
| Omit name | Signature rejects construction | Missing required contract |
| Add a cooperative counter mixin | Counter increments once | Duplicate initialization |
| Stop delegation in one participant | Downstream assertion fails | Broken chain |
| Swap compatible independent mixins | Same fields, changed trace | Hidden ordering dependency |
For the counter, keep the observed log outside the object or initialize it at one well-defined boundary. Resetting the log in multiple constructors would erase the very evidence you are collecting. Check the complete sequence and the number of endpoint visits.
Test a subclass of Job too. A base-class method may work in its original hierarchy and behave differently when reused in a larger composition. That is exactly why explaining super() in terms of runtime MRO is more reliable than naming a single parent.
A strong closing explanation is concrete: “The direct calls reached the shared initializer twice. I changed the participants to one cooperative chain, assigned each keyword to one class, rejected leftovers, and tested an added mixin.” It states the original failure, the contract, and the proof.
One more useful mutation is to give both mixins a parameter with the same name but different meanings. The first participant consumes it, leaving the second without its required input. The repair is to make ownership explicit, not to forward the consumed value to every class indiscriminately. Rename conflicting configuration fields or introduce one coordinator that validates and distributes them. This test reveals an interface-design problem that an otherwise correct MRO trace cannot diagnose on its own.