The bug took six hours of tracing through middleware layers. The fix came down to understanding how Python resolves method calls in multiple inheritance using the Method Resolution Order (MRO).
We’d built a monitoring system that combined logging, alerting, and rate-limiting behaviors into service classes. Everything worked—until two base classes defined the same method. Suddenly, alerts weren’t being sent, but no exception was raised. No crash. Just silence from a critical service.
This post explains what went wrong and how to avoid it. Multiple inheritance in Python isn’t magic. It’s deterministic. When you understand MRO, you can use multiple inheritance safely—and know exactly when to reach for mixins instead.
🧠 Understanding MRO — How Python Resolves Method Calls
Python uses the C3 linearization algorithm to compute a consistent Method Resolution Order (MRO). This isn’t left-to-right in the class list—it respects inheritance hierarchy and ensures monotonicity.
Every class has an .mro() method that returns the resolution order. When you call obj.method(), Python walks this list left to right and stops at the first class that defines the method.
Here’s an example:
class A:
def process(self):
print("A.process")
class B(A):
def process(self):
print("B.process")
class C(A):
def process(self):
print("C.process")
class D(B, C):
pass
d = D()
d.process() # What gets printed?
You might assume B.process because B appears first. Let’s check:
print(D.mro())
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
So yes, d.process() calls B.process. But swap the scenario: remove process from B.
class B(A):
pass # no process
Now D(B, C) calls C.process. The MRO hasn’t changed—[D, B, C, A]—but since B doesn’t define process, resolution continues to C.
The mechanism: method lookup walks the MRO until it finds an implementation. There’s no fallback logic, no ambiguity. It just follows the list.
Multiple inheritance isn’t dangerous if you know the MRO. It’s dangerous if you don’t check it.
🔧 MRO Gotcha: The Diamond Problem
This occurs when two parent classes inherit from the same grandparent. Without C3, you could call the grandparent method twice. Python’s MRO prevents duplication.
In our example, A appears only once in the MRO—even though both B and C inherit from it. C3 guarantees each class appears just once and in an order that respects the hierarchy.
💻 Visualizing MRO with super()
super() doesn’t mean “the parent.” It means “the next class in the MRO.” That distinction matters.
class A:
def process(self):
print("A.process")
class B(A):
def process(self):
print("B.process")
super().process()
class C(A):
def process(self):
print("C.process")
super().process()
class D(B, C):
def process(self):
print("D.process")
super().process()
D().process()
Output:
D.process
B.process
C.process
A.process
Each super() call advances along the MRO: D → B → C → A. The chain is predictable because it’s defined by the class structure.
⚙️ When MRO Fails: Inconsistent Hierarchies
If C3 can’t produce a valid order, Python raises a TypeError.
class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass # ❌ TypeError: cannot create a consistent method resolution
Output:
TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B
The conflict arises because A requires X before Y, but B requires the reverse. C3 detects the contradiction and refuses to proceed. This fails fast—better than silent misbehavior.
🧩 Mixins — Reusable Behaviors Without Deep Inheritance
A mixin is a class that adds specific functionality to others. It doesn’t stand alone. It models “can-do,” not “is-a.”
Common uses include logging, caching, serialization, or permissions. The key is that they’re state-light and designed to chain via super().
Here’s a practical example: an API client that logs, caches, and rate-limits requests.
class LoggingMixin:
def log(self, message):
print(f"[LOG] {message}")
def request(self, url):
self.log(f"Requesting {url}")
return super().request(url)
class CachingMixin:
def __init__(self):
self._cache = {}
super().__init__()
def request(self, url):
if url in self._cache:
return self._cache[url]
response = super().request(url)
self._cache[url] = response
return response
class RateLimitMixin:
def __init__(self):
from time import time
self._last_call = 0
self._rate_limit = 1 # seconds
super().__init__()
def request(self, url):
import time
now = time.time()
if now - self._last_call < self._rate_limit:
time.sleep(self._rate_limit - (now - self._last_call))
self._last_call = now
return super().request(url)
Now compose them:
class HTTPClient:
def request(self, url):
return f"Response from {url}"
class SmartClient(LoggingMixin, CachingMixin, RateLimitMixin, HTTPClient):
pass
Inspect the MRO:
print(SmartClient.mro())
[<class '__main__.SmartClient'>,
<class '__main__.LoggingMixin'>,
<class '__main__.CachingMixin'>,
<class '__main__.RateLimitMixin'>,
<class '__main__.HTTPClient'>,
<class 'object'>]
Call the client:
client = SmartClient()
print(client.request("https://api.example.com/data"))
print(client.request("https://api.example.com/data")) # cached
Output:
[LOG] Requesting https://api.example.com/data
Response from https://api.example.com/data
[LOG] Requesting https://api.example.com/data
Response from https://api.example.com/data
The second call still logs and checks rate limits—but hits the cache. The behavior chain runs in MRO order, each super() advancing to the next.
This is a valid use of multiple inheritance. Each mixin adds one concern. The MRO is clear. And super() chains work as intended.
🔧 Mixin Design Rules
- Always call
super().**init**()in**init**. - Assume required methods exist in classes later in the MRO.
- Keep mixins focused—only one behavior per mixin.
- Use the
Mixinsuffix for clarity.
💡 When Not to Use Mixins
Avoid mixins when:
- The base class doesn’t support
super()chaining. - Two mixins define conflicting state (e.g., both use
_data). - You need multiple methods to run unconditionally—mixins override, not combine.
📦 Composition vs. Inheritance — Choosing the Right Tool
Inheritance implies “is-a.” Composition implies “has-a.” Composition often wins for clarity and testability.
Here’s the same functionality using composition:
class Logger:
def log(self, message):
print(f"[LOG] {message}")
class Cache:
def __init__(self):
self._cache = {}
def get(self, key, fetch_fn):
if key not in self._cache:
self._cache[key] = fetch_fn()
return self._cache[key]
class RateLimiter:
def __init__(self, rate_limit=1):
self._last_call = 0
self._rate_limit = rate_limit
def limit(self):
import time
now = time.time()
if now - self._last_call < self._rate_limit:
time.sleep(self._rate_limit - (now - self._last_call))
self._last_call = now
class HTTPClient:
def __init__(self):
self.logger = Logger()
self.cache = Cache()
self.rate_limiter = RateLimiter(rate_limit=1)
def request(self, url):
self.logger.log(f"Requesting {url}")
self.rate_limiter.limit()
return self.cache.get(url, lambda: f"Response from {url}")
No MRO. No super(). Just direct method calls. Easier to debug. Easier to test. More control.
Use multiple inheritance when:
- You’re applying cross-cutting behaviors via well-designed mixins.
- You’re extending frameworks that expect it (e.g., Django views).
- The method chain is intentional and
super()is used consistently.
Use composition when:
- Execution order must be explicit.
- You’re combining stateful components.
- The MRO feels like a liability.
🧠 Mechanism: super() is Dynamic
super() doesn’t hardcode the next class. It uses the MRO of the instance’s class , not the class where the method is defined.
That means LoggingMixin behaves correctly in different inheritance contexts—because super() always follows the actual MRO at runtime.
🚀 Real-World Example — Django Forms and Mixins
Django’s class-based views rely heavily on multiple inheritance and mixins.
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import CreateView
from myapp.models import Task
class TaskCreateView(LoginRequiredMixin, CreateView):
model = Task
fields = ['title', 'due_date']
LoginRequiredMixin.dispatch runs first. It checks authentication, then delegates to super().dispatch(), which eventually reaches CreateView.dispatch.
The MRO ensures the security check runs before any view logic.
Debug it:
print(TaskCreateView.mro())
Output:
[<class 'myapp.views.TaskCreateView'>,
<class 'django.contrib.auth.mixins.LoginRequiredMixin'>,
<class 'django.views.generic.edit.CreateView'>,
...]
LoginRequiredMixin appears first in the MRO—so its dispatch takes precedence.
🔧 Debugging MRO in Django
If a mixin seems to be ignored, run .mro() and verify its position. If it’s after the target class, it won’t intercept the method.
💡 Avoiding Conflicts
Never define form_valid() in two mixins unless you intend one to override the other. If you need multiple behaviors on form save, use signals or refactor to composition.
🟩 Final Thoughts
Multiple inheritance is a tool, not a trap. The issue isn’t the feature—it’s using it without understanding the MRO.
I’ve seen teams ban it after one bug. That’s overcorrection. Learn how it works. Check YourClass.mro() early and often. Use it with mixins that are designed for chaining.
More than that: understanding MRO makes you a better reader of code. You’ll predict behavior. You’ll debug faster. You’ll design systems that are both flexible and maintainable.
Just run print(YourClass.mro()). Five seconds. Prevents six hours.
❓ Frequently Asked Questions
Can I use multiple inheritance with **init** methods?
Yes, but only if all classes use super() consistently. Direct calls like Parent.__init__(self) break the chain and can cause skipped or duplicated calls.
📑 Table of Contents
- 🧠 Understanding MRO — How Python Resolves Method Calls
- 🔧 MRO Gotcha: The Diamond Problem
- 💻 Visualizing MRO with super()
- ⚙️ When MRO Fails: Inconsistent Hierarchies
- 🧩 Mixins — Reusable Behaviors Without Deep Inheritance
- 🔧 Mixin Design Rules
- 💡 When Not to Use Mixins
- 📦 Composition vs. Inheritance — Choosing the Right Tool
- 🧠 Mechanism: super() is Dynamic
- 🚀 Real-World Example — Django Forms and Mixins
- 🔧 Debugging MRO in Django
- 💡 Avoiding Conflicts
- 🟩 Final Thoughts
- ❓ Frequently Asked Questions
- Can I use multiple inheritance with
**init**methods? - What happens if two parent classes define the same method?
- Are mixins the same as abstract base classes?
- 📚 References & Further Reading
What happens if two parent classes define the same method?
Python uses the MRO to decide. The first class in the list that defines the method is used. You can inspect the order with ClassName.mro().
Are mixins the same as abstract base classes?
No. Mixins provide reusable implementation. Abstract base classes define interfaces and enforce method contracts. A class can use both for different purposes.
📚 References & Further Reading
- Official Python docs on multiple inheritance and MRO: docs.python.org
- Django class-based views and mixin patterns: docs.djangoproject.com
- Python data model and super() behavior: docs.python.org

Top comments (0)