Multiple Inheritance — Method Override Corrupted Records
Missing timestamps & duplicated user IDs — a mixin's prepare() overrode Transaction's init via MRO.
20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.
- Multiple inheritance composes behavior from multiple parent classes — each method resolved via the MRO at call time
- C3 linearization produces a deterministic, monotonic MRO — inspect it with
ClassName.__mro__before combining framework classes - The diamond problem (two parents sharing a common ancestor) is resolved consistently by C3, not depth-first search
- Mixins are the production-safe pattern: small, focused classes that add orthogonal behavior like logging or serialization
- Cooperative
super().__init__(**kwargs)is mandatory — skip it and parent classes silently skip initialization - 90% of MRO bugs come from not checking
__mro__when combining third-party framework classes - MRO resolution cost is O(m) where m is MRO length — negligible for most but measurable in hot loops with deep hierarchies
Imagine you're a kid who inherited your mum's musical talent AND your dad's athletic ability. You didn't have to pick one — you got both. Multiple inheritance works the same way: a class can inherit attributes and methods from more than one parent class at once. The tricky part Python had to solve is: what happens when both parents have a method with the same name? Whose version do you use? That's exactly the puzzle this article unpacks.
Multiple inheritance is one of Python's most powerful — and most misunderstood — features. In production codebases, it powers plugin architectures, Django's class-based views, and protocol composition at scale. Understanding it deeply separates engineers who reason about class hierarchies from those who copy-paste until something breaks.
The core problem multiple inheritance solves is code reuse across orthogonal concerns. A LoggableMixin handles logging. An AuditableMixin handles audit trails. A SerializableMixin handles JSON output. Your final UserService class inherits all three — clean, testable, composable. But get the MRO wrong and you'll silently override critical methods.
By the end you'll understand C3 linearization well enough to predict MRO by hand, design mixin hierarchies that don't surprise you in production, avoid the bugs that bite experienced engineers, and answer the interview questions that filter for senior candidates.
Don't guess your MRO. Inspect it. That's the difference between a senior engineer and a mid-level one. Here's the exact command: print(ClassName.__mro__). Now let's see why C3 linearization makes this predictable.
What is Multiple Inheritance in Python?
Multiple Inheritance in Python is a core concept where a class can derive from more than one base class. This allows the subclass to inherit attributes and methods from all listed parents. It's a form of composition that enables powerful code reuse — but it also introduces complexity. When two parents define the same method, Python must decide which version to call. That decision is made by the Method Resolution Order (MRO).
Here's the thing most tutorials don't tell you: multiple inheritance works great when parents are orthogonal — a Logger and a Sender have nothing in common, so there's no conflict. The problems start when two parents share method names. That's when you need MRO.
Here's a minimal example:
```python # io.thecodeforge.multiple_inheritance_basic class Logger: def log(self, message): print(f"Logger: {message}")
class Sender: def send(self, data): print(f"Sender: {data}")
class Service(Logger, Sender): def run(self): self.log("Starting service") self.send("Sending status")
s = Service() s.run() print(Service.__mro__) ```
If you're building a class that inherits from two framework classes, stop and check the MRO first. We've seen Django view mixins that silently override each other because someone assumed the parent order mattered more than the actual linearization. It doesn't work that way.
Here's a real-world extension: imagine you're adding a MetricsMixin to a payment handler. That mixin might define to log processing time, but the base payment handler also defines process(). Without checking process()__mro__, you'll get the mixin's process when you wanted the parent's. Always print the MRO before assuming.
save() method in a mixin overrode a database save logic in the main class — no errors, just silently wrong data.__mro__ and check for overlapping method names.__mro__ when combining framework classes.save() override – it's cheap and it works.__mro__ when combining classes that may share method names — guessing is how data gets corrupted.ClassName.__mro__ before assuming which method runs.Method Resolution Order (MRO) and C3 Linearization
Python's MRO is determined by the C3 linearization algorithm. It produces a linear order of all ancestor classes that respects three rules: monotonicity (if a class appears before another in one linearization, it must appear before in all related ones), local precedence order (parents are listed in the order they appear in the class definition), and consistency (the order must be a valid topological sort of the inheritance graph).
Don't let the academic description scare you. Here's the practical intuition: Python builds the MRO by merging the parent class MROs left to right, picking the first class that isn't blocked by appearing in the tail of any later list. It's like merging multiple sorted lists while preserving each list's relative order.
Let's see C3 in action with a more complex hierarchy:
```python # io.thecodeforge.mro_c3_linearization class A: def identify(self): return "A"
class B(A): def identify(self): return "B"
class C(A): def identify(self): return "C"
class D(B, C): pass
d = D() print(d.identify()) print(D.__mro__) ```
The monotonicity guarantee is what makes C3 safe: if a class appears before another in one hierarchy, it will always appear before in any subclass. That's not true for depth-first search. Python's design chose predictability over simplicity.
Here's a mental model to internalise C3: when merging, look at the first element of each parent's MRO list that isn't in the tail of any other list. If multiple candidates exist, pick the one from the leftmost parent. This ensures that B comes before C in our example because B is the first parent of D. If you ever need to predict the MRO manually, use this merge process.
- Python builds the linearization recursively, merging parent MROs.
- The local precedence order (the order you write parents in the class definition) is preserved as much as possible.
- Monotonicity means that if class X appears before Y in the MRO of a child, it will appear before Y in the MRO of any further subclass.
- If C3 can't produce a valid merge, Python raises a
TypeError— your hierarchy is simply inconsistent.
View and UpdateView with a custom mixin can shift method resolution in non-obvious ways.print(ClassName.__mro__) — we caught a production bug this way where a mixin placed after a framework class was silently ignored due to an unexpected C3 merge.class MyView(Mixin, ListView). They assumed Mixin would win, but ListView's own MRO placed a get() from its parent before Mixin's get(). Always verify.__mro__ to inspect — never guess. Guessing is how silent data corruption happens.ClassName.__mro__ before making assumptions about which method runs.super() call is not reaching the expected parentsuper().__init__ calls by adding print statements and verify against MRO.The Diamond Problem – When Inheritance Gets Complicated
The diamond problem occurs when a class inherits from two classes that share a common ancestor. The classic shape: D inherits from B and C, both of which inherit from A. If both B and C override a method from A, which version does D use? Python handles this elegantly via C3 linearization – it ensures the method is resolved in a consistent order.
Here's the intuition: Python doesn't do depth-first search like C++ did in the bad old days. Instead, C3 ensures each ancestor appears exactly once in the MRO, and the order respects both local precedence (the order you wrote the parents) and monotonicity (consistent ordering across the hierarchy).
Example:
```python # io.thecodeforge.diamond_problem class A: def greet(self): return "Hello from A"
class B(A): def greet(self): return "Hello from B"
class C(A): def greet(self): return "Hello from C"
class D(B, C): pass
d = D() print(d.greet()) #? Output: Hello from B print(D.__mro__) # D -> B -> C -> A -> object ```
The real trap is when you mix third-party libraries. Their base classes might already have internal diamonds you didn't plan for. Always inspect MRO before combining.
Here's a twist: what if B and C don't override the same method, but both override different methods that call ? The cooperative chain can create unexpected call orders. We'll see that in the cooperative super section.super().greet()
C to be resolved before A because C is the second parent. But MRO depends on the full linearization, not just the sibling order. Always check the actual MRO.UpdateView with CreateView can create a diamond if not done carefully.__mro__ and verify that your mixin appears where you expect it.get_context_data returned empty dict—turned out a diamond caused the wrong method to run. MRO dump solved it in seconds.__mro__ and look.Diamond Problem Hierarchy Diagram
Visualising the diamond problem helps build an intuitive understanding of why C3 linearization is necessary. The classic diamond structure looks like this: a base class A at the top, two child classes B and C inheriting from A, and a derived class D inheriting from both B and C. This forms a diamond shape in the inheritance graph.
In the diagram below, the solid arrows indicate direct inheritance. The dashed arrow shows the C3 linearization path: D -> B -> C -> A -> object. Python does not traverse depth-first (which would be D -> B -> A -> C -> A with duplicates), but rather uses a merge that produces a unique, consistent order.
This structure is common in real frameworks: for example, Django's UpdateView and CreateView both inherit from FormMixin, and a custom MyFormView(UpdateView, CreateView) creates a diamond. Understanding the diagram helps you predict which method gets called when both UpdateView and CreateView override the same method.
get_form_class() to resolve to the wrong parent, breaking form validation for an entire API endpoint.__mro__ and verify the order of the diamond branches.Mixin Design Patterns in Production
Mixins are the cleanest production use case for multiple inheritance. A mixin is a small class that provides a specific, reusable behaviour through methods and should not be instantiated on its own. Mixins often override or augment methods in a specific way, and they rely on the final class to supply the rest of the required interface.
The golden rule of mixins: each mixin should do exactly one thing. A JsonMixin adds JSON serialization. A LogMixin adds logging. An AuthMixin adds authentication checks. Never combine responsibilities in a single mixin — that's what concrete classes are for.
Here's a JSON serializable mixin that works with any class that has to_dict:
```python # io.thecodeforge.mixin_pattern class JsonMixin: def to_json(self): import json if hasattr(self, 'to_dict'): return json.dumps(self.to_dict()) raise AttributeError("Class must define to_dict()")
class User: def __init__(self, name, age): self.name = name self.age = age def to_dict(self): return {'name': self.name
Mixin or prefix like With. This helps other developers understand the intent and avoid accidental collisions.self.data existed and failed silently — leading to corrupted exports for six months before anyone noticed.LogMixin that assumes self.logger is set will break if the concrete class uses self.logger for something else — avoid name overlap, prefix with mixin_.Real-World Gotchas and Debugging Tips
Even experienced developers run into multiple inheritance pitfalls. Here are the most common gotchas you'll face in production:
- Argument order in
: When each parent class requires different arguments, designing cooperativesuper().__init____init__methods that pass along unknown kwargs is critical. - Method shadowing by mixins: A mixin that defines a method with the same name as a method in another parent silently overrides it.
- MRO surprises due to library classes: When your class inherits from a mixin and a library base class that itself uses multiple inheritance, the MRO can become non-intuitive.
- Cyclic dependencies: Python's MRO algorithm detects invalid inheritance patterns (like cycles) and raises a
TypeError. - Inconsistent MRO (TypeError): If C3 cannot linearize the hierarchy, you get a
TypeErrorat class creation. This often happens when you try to inherit from two classes that both inherit from each other or have conflicting MROs.
Here's the thing about the __init__ chain: if any class in the hierarchy forgets to call , every class after it in the MRO is silently skipped. No error. Just uninitialized attributes. This is the most common multiple inheritance bug in production.super().__init__()
Let's look at a cooperative init pattern:
```python # io.thecodeforge.cooperative_init class A: def __init__(self, a, kwargs): self.a = a super().__init__(kwargs)
class B: def __init__(self, b, kwargs): self.b = b super().__init__(kwargs)
class C(A, B): def __init__(self, c, kwargs): self.c = c super().__init__(kwargs)
obj = C(a=1, b=2, c=3) print(obj.a, obj.b, obj.c) print(C.__mro__) ```
Debugging the __init__ chain is the most painful part of multiple inheritance. We've traced bugs that boiled down to one missing **kwargs in a parent class written three years ago. The fix: add a debug print to every __init__ during development.
**kwargs in __init__ and pass them to super().__init__(). This lets each class in the MRO chain extract its own parameters and pass the rest.super() is a common source of bugs.**kwargs or pass them wrong, some parent class may miss its initialization arguments.__init__ consumed user_id but never passed request to the next class — breaking request lifecycle.__init__ chain.__init__ in development to confirm the order of execution.__init__, the cooperative chain breaks because kwargs order changes. Always use keyword arguments.super().__init__ with **kwargs in every class that uses multiple inheritance.__init__ call chain — one missing super() call and the rest of the hierarchy silently fails.Understanding super() Navigation Logic and Common Pitfalls
The builtin is how Python navigates the MRO to call a method from the next class in the chain. It doesn't just call the parent — it follows the entire MRO. This is called "cooperative super" because each class cooperates by calling super() to pass control to the next class. If any class breaks the chain (by not calling super()), all subsequent classes are silently skipped.super()
Here's the navigation logic: in a method body returns a proxy object that, when you call a method on it, looks up the method in the classes that appear after the current class in the MRO. For example, if the MRO is super()[D, B, C, A, object], then in class super()D will find methods in class B, in class super()B will find methods in class C, and so on. This chain is not the same as the direct parent — it's the full linearization.
Common pitfalls:
- Assuming
calls the direct parent: In single inheritance it does, but in multiple inheritance it follows the MRO. Always know the MRO.super() - Missing
call in a mixin: If a mixin overrides a method and doesn't callsuper(), it terminates the chain. The next class in the MRO never runs its version.super() - Incorrect
kwargs handling: If a method consumes a keyword argument but doesn't pass it throughkwargs, the argument is lost for downstream classes. - Using
with only two arguments incorrectly: You rarely need to pass explicit arguments tosuper()in Python 3 — justsuper()works.super().method()
Let's see a correct vs broken example:
```python # io.thecodeforge.super_navigation class InitBase: def __init__(self, **kwargs): print("InitBase.__init__: kwargs =", kwargs) # no super() — should be called first? Actually it's last in MRO for object
class LogMixin: def __init__(self, kwargs): print("LogMixin.__init__: kwargs =", kwargs) super().__init__(kwargs)
class AuthMixin: def __init__(self, kwargs): print("AuthMixin.__init__: kwargs =", kwargs) super().__init__(kwargs)
class Service(AuthMixin, LogMixin, InitBase): def __init__(self, kwargs): print("Service.__init__: kwargs =", kwargs) super().__init__(kwargs)
s = Service(user="alice", token="abc") print(Service.__mro__) ```
If any class (e.g., LogMixin) had forgotten the call, super()InitBase would never set up user. That's why cooperative is mandatory in production hierarchies.super()
To debug, add a simple print to every __init__ and watch the order of execution. If the chain stops unexpectedly, look for a missing call.super()
A more advanced pitfall: in a class that is not part of the MRO of the final class? That can't happen because super() is bound to the instance's class, which has the dynamic MRO. But be careful when using super() inside a nested scope or a decorator — the class might change.super()
Finally, note that works for any method, not just super()__init__. The same cooperative chain applies to __getattr__, __setattr__, or any custom method. If you have a chain of overrides, each should call process() to allow the chain to continue.super().process()
super(), the chain breaks. All subsequent classes are silently skipped. Always call super().method() unless you explicitly intend to terminate the chain.super().__init__() caused a parent class to never initialize its attributes, leading to AttributeError at runtime.__init__ in the hierarchy calls super().__init__(**kwargs).__init__ chain completes for all expected attributes.CacheMixin.__init__ consumed timeout without passing it through, and the next parent DBMixin silently died because it couldn't find timeout. The error was a cryptic TypeError from deep inside the ORM. The fix: add **kwargs and pass it through.super() are a recipe for disaster.super() follows the MRO, not the direct parent.super() in overridden methods to maintain the cooperative chain.**kwargs to pass unused arguments to the next class — breaking the chain causes silent failures.Testing MRO with Unit Tests
One of the most overlooked practices in multiple inheritance is adding unit tests that verify the MRO. A change to a parent class or the order of parents can silently shift method resolution, and you won't notice until a subtle bug surfaces in production.
Write tests that assert the MRO tuple contains the expected classes in the expected order. For example:
``python # io.thecodeforge.test_mro_assertion class TestMRO: def test_mro_order(self): expected = [Service, Logger, Sender, object] actual = list(Service.__mro__) assert actual == expected, f"MRO mismatch: {actual}" ``
Also, test that specific methods resolve to the correct class. Use method.__qualname__ to check the origin:
``python assert Service.log.__qualname__ == 'Logger.log' ``
Add these tests whenever you define a class with multiple inheritance. They act as regression detectors. If someone reorders parents or adds a new mixin, the test will fail, making the change explicit.
Django's class-based views documentation recommends similar checks using getattr(cls, 'method') and comparing the function's __module__. You can do the same.
__mro__ in the test suite would have caught it before deployment.Building Composable Mixin Hierarchies with ABCs
When you have multiple mixins that depend on each other, you need a way to enforce contracts. Python's abc module lets you define abstract base classes that require subclasses to implement specific methods. Combine this with multiple inheritance to build robust, composable hierarchies.
Here's why this matters: without ABCs, a mixin that calls just hopes the method exists. If it doesn't, you get an self.to_dict()AttributeError at runtime — possibly in production, under a specific code path that only happens once a month. With ABCs, that error moves to instantiation time: Python refuses to create the object if the contract isn't satisfied.
Here's an example where a SerializableMixin expects to exist, and we enforce that with an ABC:to_dict()
```python # io.thecodeforge.abc_mixin from abc import ABC, abstractmethod
class DataMixin(ABC): @abstractmethod def to_dict(self): pass
class JsonMixin(DataMixin): def to_json(self): import json return json.dumps(self.to_dict())
class User(JsonMixin): def __init__(self, name): self.name = name def to_dict(self): return {'name': self.name}
u = User('Alice') print(u.to_json()) # Works
# User2 would fail on instantiation if to_dict missing: # class BadUser(JsonMixin): pass # b = BadUser() # TypeError: Can't instantiate abstract class ```
ABCs make your mixin contracts explicit. Without them, you're relying on the caller to read documentation. With them, Python enforces it at class creation time. That's a huge win for team codebases.
- Define an ABC with abstract methods that mixins need.
- Make the mixin inherit from the ABC directly.
- Any class that uses the mixin must implement the abstract methods or be abstract itself.
- This pattern catches missing methods at instantiation time — before they cause silent data corruption in production.
self.to_dict() and hopes it exists.@abstractmethod on the final method in the MRO.When to Use Composition Instead of Multiple Inheritance
Multiple inheritance is powerful, but it's not always the right tool. The classic design principle "favor composition over inheritance" applies double here. Every time you reach for multiple inheritance, ask yourself: could this be composition instead?
Composition means your class holds a reference to another class and delegates work to it. Instead of class User(JsonMixin), you write class User: self.serializer = . The trade-off: more boilerplate, but zero risk of method collision, zero MRO surprises, and much simpler testing because you can mock the composed object.JsonSerializer()
Here's a practical decision rule: use multiple inheritance when the mixins add orthogonal behavior that truly needs to be part of the class itself (like __init__ hooks or operator overloading). Use composition when you're just calling utility methods on data — a JsonSerializer doesn't need to be a parent, it can just be an attribute.
Example showing composition vs inheritance:
```python # io.thecodeforge.composition_vs_inheritance # Composition approach — no MRO risk class JsonSerializer: @staticmethod def to_json(data): import json return json.dumps(data)
class User: def __init__(self, name): self.name = name self.serializer = JsonSerializer()
def to_dict(self): return {'name': self.name}
def to_json(self): return self.serializer.to_json(self.to_dict())
u = User("Alice") print(u.to_json()) # Same result, zero MRO complexity ```
If you find yourself inheriting from more than three mixins, stop and ask: could I compose instead? The answer is almost always yes.
- Inheritance creates tight coupling — changing a parent affects all children.
- Composition creates loose coupling — the composed object can be swapped at runtime.
- Multiple inheritance is appropriate for mixins that need access to
selfinternals. - Composition is better for utility behavior that operates on public interfaces only.
- When in doubt, start with composition. It's easier to refactor to inheritance later than the reverse.
class PaymentHandler(LoggerMixin, CacheMixin, AuthMixin, ValidatorMixin), compose self.logger = Logger(), self.cache = Cache(), etc.__init__ or access private attributes of the classComposition vs Inheritance (Mixins) Comparison Table
When deciding between composition and mixin-based multiple inheritance, it helps to have a side-by-side comparison of the key trade-offs. The table below covers the most important aspects for production code.
| Aspect | Multiple Inheritance (Mixin) | Composition |
|---|---|---|
| Coupling | Tight — a change in a parent class can break all subclasses. | Loose — the composed object can be replaced or mocked independently. |
| MRO Complexity | Must understand C3 linearization to predict method resolution. | No MRO — direct delegation to a specific object. |
| Testing | Harder to mock because methods are inherited; need to test MRO chain. | Easy to mock the composed dependency with a test double. |
| Code Reuse | Methods are inherited automatically — no extra boilerplate. | Requires explicit delegation calls for each method. |
| Initialization Hooks | Can override __init__ and other special methods to hook into the object lifecycle. | Cannot hook into the object lifecycle — initialization is done in the composing class. |
| Method Collision Risk | High — common method names (like prepare, init, save) can collide silently. | None — method names are scoped to the composed object; no collision with other mixins. |
| Production Debugging Speed | Slow — need to trace MRO and chain to find which method actually runs. | Fast — just check the delegation call; no complex chain. |
| Refactoring Ease | Difficult — changing parent methods can have unintended consequences across the hierarchy. | Easy — you can swap the composed object or change its interface without affecting other classes. |
| Use Case Fit | Best for orthogonal concerns that need to access self internals (e.g., logging, authentication with state). | Best for stateless utility behavior (e.g., serialization, validation) or when you need testability. |
The key takeaway from this table: use composition when you can, inheritance when you must. Start with composition for any new behavior, and only switch to a mixin if you find that composition leads to excessive boilerplate or you need to hook into __init__.
In practice, we've found that about 80% of cases where developers reach for multiple inheritance can be replaced with composition, resulting in simpler, more maintainable code. The remaining 20% are genuine mixin use cases — and for those, the safety practices in this article (inspect MRO, use cooperative super, write MRO tests) are non-negotiable.
Auditor object. Debugging time dropped to minutes.self.__init__ or operator overloading.super(), and write MRO tests.Anti-Patterns: When Multiple Inheritance Bites Back
Even with all the right practices, there are common anti-patterns that turn multiple inheritance into a maintenance nightmare. Here are the ones we see most often in production code:
1. The God Mixin – A single mixin that does logging, caching, serialization, and validation. It violates single responsibility and creates a massive implicit dependency. Break it into focused mixins.
2. The Copy-Paste Hierarchy – Someone copies a class hierarchy from another project without understanding the MRO. Result: methods resolve to unexpected parents. The fix: always inspect __mro__ after adapting a hierarchy.
3. The Silent Killer – A mixin that overrides a method without calling super() deliberately because "it's just a mixin". This silences the entire chain. Every override in a mixin should either call super() or be documented as intentionally terminal.super()
4. The Generic Name Trap – Using , run(), init() in mixins. These collide with concrete class methods constantly. Prefix all mixin methods with setup()mixin_ or use a namespaced convention.
5. The Framework Blindness – Assuming that because your class inherits from two framework base classes, the MRO will be predictable. Framework classes often have multiple inheritance internally, creating diamonds you can't see. Always dump __mro__.
Here's an example of the generic name trap:
```python # io.thecodeforge.anti_pattern_generic_name class LogMixin: def setup(self): # 'setup' is too generic print("LogMixin setup")
class CacheMixin: def setup(self): # same generic name print("CacheMixin setup")
class Service(LogMixin, CacheMixin): def setup(self): super().setup() # Which setup? Depends on MRO print("Service setup")
s = Service() s.setup() print(Service.__mro__) ```
The MRO determines which runs first. If you assumed setup()LogMixin then CacheMixin, you might be surprised.
Rule of three: if you have more than three mixins, your architecture probably needs a rethink. Two to three is manageable. Five is a red flag.
CacheMixin.setup() accidentally overrode a DBConnectionPool.setup() — the cache mixin was listed first in the class definition.mixin_setup() and always dump MRO.__private mangling for mixin methods – it makes debugging harder and doesn't prevent collisions between sibling classes.__mro__ when adapting existing hierarchies — surprises are expensive.run, init, setup__mro__ and test method resolution for every method used.super()super() to maintain the chain.The Super Function Isn't Magic — It's a Dispatch Table
Most devs think just 'calls the parent method.' That assumption will burn you in production. super() doesn't call the parent. It calls the next class in the MRO.super()
That distinction matters when you're debugging a chain of five mixins and the wrong one is swallowing a call. navigates a linked list of classes built by C3 linearization. It hands off to the next class in the list, regardless of inheritance hierarchy.super()
If you define __init__ in every mixin and forget to call super().__init__(), the chain breaks silently. Objects initialize partially. Data goes missing. Good luck tracing that at 2 AM.
The rule: In every method you expect to be overridden in a child, call . Even if this class is the 'base.' That ensures the chain propagates, not terminates. super().method()object. handles being called by all classes in the chain — it's a no-op. But if you skip a link? You kill the chain.__init__()
super().__init__(), the remaining classes are never called. No error. Just broken state. Use a debugger breakpoint on super().__init__() to verify the entire chain fires.The Parent Constructor Is Your Single Point of Catastrophe
Multiple inheritance with different __init__ signatures is a slow-motion train wreck. Each mixin expects its own arguments. The final child class must satisfy all of them — and you can't miss a single one.
Look at the code. AuthMixin expects user. LogMixin doesn't care about args. The child RequestHandler takes user and endpoint. But when is called, it passes super().__init__user as a keyword argument. That keyword propagates through AuthMixin (uses it) and then LogMixin (ignores it). It works because Python allows args, *kwargs to soak up extra params.
But the moment you have two mixins that both want user but spell it differently? user vs username? You get a TypeError at runtime. No static analysis catches this. And the error message only shows you the last class that failed, not the whole chain.
The fix: standardize argument names across all mixins. Use **kwargs everywhere. Only unpack what you need. Or, better yet: pass a unified config dictionary. No positional arg hell.
The silent method override that corrupted millions of records
BaseLogger mixin defined a prepare() method that collided with a method in the Transaction parent. The MRO resolved to the mixin's version first, skipping the transaction's critical initialization.super().prepare() inside the mixin, and added an automated test that checks __mro__ for unexpected method resolution.- Never assume MRO order — always inspect
ClassName.__mro__when combining multiple parents, especially if they define the same method name. - Use meaningful method names in mixins to reduce collision risk (prefix with
mixin_or similar convention). - Add a unit test that explicitly tests the MRO tuple and verifies method resolution matches your mental model.
ClassName.__mro__ to see the full resolution order. Compare with your expectation. If the order is wrong, reorder the parent list in the class definition.super() to delegate, or rename the mixin method with a prefix like _mixin_.super() calls are not reaching the expected next class in MROsuper().__init__ and trace the MRO chain using a simple print. Remember that super() follows the MRO, not the obvious linear hierarchy.AttributeError on an attribute you expected to be initialized__init__ in the hierarchy calls super().__init__(**kwargs). One missing super() call breaks the entire initialization chain.TypeError with unexpected argument signature__init__ methods use **kwargs to pass through unknown parameters. A method consuming a named arg without forwarding it will break downstream initializers.python print(ClassName.__mro__)python print(*[c.__name__ for c in ClassName.__mro__], sep=' -> ')Common mistakes to avoid
5 patternsForgetting to call super().__init__() in a mixin
AttributeError when accessing self.attribute at runtime, but only in certain call paths.__init__ calls super().__init__(kwargs). Accept kwargs in every __init__ to pass through unknown parameters.Assuming parent order in the class definition equals MRO
ClassName.__mro__ to see the actual resolution order. Remember MRO is built recursively from parent classes, not just the linear list in your class definition.Not using `**kwargs` in cooperative __init__
TypeError: __init__() got an unexpected keyword argument. Or arguments are lost and attributes never set.__init__ methods in a multiple inheritance chain must accept kwargs and pass them to super().__init__(kwargs). Do not consume arguments without forwarding the rest.Using generic method names in mixins without prefixes
save() or prepare() unexpectedly overrides a method in the concrete class or another parent, causing data corruption or skipped logic.mixin_ or use a distinct naming convention. At minimum, document which methods the mixin provides and which it expects.Not testing MRO when combining framework classes
ClassName.__mro__ and asserts the position of key classes. Run this test after every change to the class hierarchy.Interview Questions on This Topic
What is the diamond problem in multiple inheritance and how does Python resolve it?
ClassName.__mro__.Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Everything here is grounded in real deployments.
That's OOP in Python. Mark it forged?
17 min read · try the examples if you haven't