DEV Community

Python-T Point
Python-T Point

Posted on • Originally published at pythontpoint.in

🐍 python multiple inheritance examples — common mistakes and how to fix them

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.

python multiple inheritance examples

🧠 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?
Enter fullscreen mode Exit fullscreen mode

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'>]
Enter fullscreen mode Exit fullscreen mode

So yes, d.process() calls B.process. But swap the scenario: remove process from B.

class B(A):
    pass  # no process
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

Output:

D.process
B.process
C.process
A.process
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Output:

TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Now compose them:

class HTTPClient:
    def request(self, url):
        return f"Response from {url}"

class SmartClient(LoggingMixin, CachingMixin, RateLimitMixin, HTTPClient):
    pass
Enter fullscreen mode Exit fullscreen mode

Inspect the MRO:

print(SmartClient.mro())



[<class '__main__.SmartClient'>,
 <class '__main__.LoggingMixin'>,
 <class '__main__.CachingMixin'>,
 <class '__main__.RateLimitMixin'>,
 <class '__main__.HTTPClient'>,
 <class 'object'>]
Enter fullscreen mode Exit fullscreen mode

Call the client:

client = SmartClient()
print(client.request("https://api.example.com/data"))
print(client.request("https://api.example.com/data"))  # cached
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 Mixin suffix 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}")
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

Output:

[<class 'myapp.views.TaskCreateView'>,
 <class 'django.contrib.auth.mixins.LoginRequiredMixin'>,
 <class 'django.views.generic.edit.CreateView'>,
 ...]
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)