AiTechWorlds
AiTechWorlds
Imagine a university professor named Dr. Patel. She is both a teacher (she designs courses, delivers lectures, grades assignments) and a researcher (she writes papers, runs experiments, applies for grants). She did not choose one role — she inherited the skills and responsibilities of both.
Now, a student asks Dr. Patel: "Can you explain quantum entanglement to me?"
She could answer as a teacher — slowly, with analogies and diagrams. Or she could answer as a researcher — precisely, with equations and citations.
Both roles have an "explain" method. Which one does she use? That conflict is the heart of the Multiple Inheritance Problem in OOP.
Python solves it with a clear algorithm called the Method Resolution Order (MRO).
class Teacher:
def greet(self):
return "Hello, I am a teacher."
def explain(self):
return "Let me use an analogy..."
class Researcher:
def greet(self):
return "Hello, I am a researcher."
def explain(self):
return "According to the literature..."
class Professor(Teacher, Researcher): # Inherits from BOTH
pass
prof = Professor()
print(prof.greet()) # Which greet() does Python use?
print(prof.explain()) # Which explain()?
Output:
Hello, I am a teacher.
Let me use an analogy...
Python chose Teacher's methods. But why — and how can you predict it?
The diamond problem appears when two parent classes share a common ancestor:
Base
/ \
Left Right
\ /
Child
class Base:
def hello(self):
print("Base hello")
class Left(Base):
def hello(self):
print("Left hello")
class Right(Base):
def hello(self):
print("Right hello")
class Child(Left, Right):
pass
c = Child()
c.hello() # Which hello() runs?
Output:
Left hello
Without a resolution rule, Python would face an ambiguity: Left.hello or Right.hello, and should Base.hello run once or twice? The MRO eliminates that ambiguity.
Python uses the C3 linearization algorithm (introduced in Python 2.3) to produce a flat, ordered list of classes to search through when looking up any method or attribute.
The rule in plain English:
Search the class itself first, then its parents left-to-right, then their parents — but never visit a class until all classes that declared it as a parent have already been searched.
You can inspect the MRO directly:
print(Child.__mro__)
# (<class 'Child'>, <class 'Left'>, <class 'Right'>, <class 'Base'>, <class 'object'>)
Or in a more readable format:
for cls in Child.__mro__:
print(cls.__name__)
Output:
Child
Left
Right
Base
object
Python walks this list from top to bottom and uses the first class that defines the method. For Child.hello():
Child — no hello defined hereLeft — hello found — use this one, stop searchingRight.hello and Base.hello are never reached.
class Base:
def describe(self):
return "I am the base class."
class JsonMixin:
def serialize(self):
return f'{{"description": "{self.describe()}"}}'
class HtmlMixin:
def render(self):
return f"<p>{self.describe()}</p>"
class Report(JsonMixin, HtmlMixin, Base):
def describe(self):
return "Annual Sales Report"
r = Report()
print(r.describe()) # Uses Report's override
print(r.serialize()) # JsonMixin.serialize calls self.describe()
print(r.render()) # HtmlMixin.render calls self.describe()
print()
for cls in Report.__mro__:
print(cls.__name__)
Output:
Annual Sales Report
{"description": "Annual Sales Report"}
<p>Annual Sales Report</p>
Report
JsonMixin
HtmlMixin
Base
object
self.describe() inside JsonMixin.serialize resolves to Report.describe because the MRO starts from the actual object's class (Report), not from JsonMixin. This is the key to why mixins work elegantly in Python.
A mixin is a class designed to be mixed into other classes, not used on its own. It provides a focused set of methods and deliberately inherits from nothing (or just object). It has no __init__ and does not hold its own state.
class LogMixin:
"""Add logging capability to any class."""
def log(self, message):
print(f"[LOG] {self.__class__.__name__}: {message}")
class TimestampMixin:
"""Add timestamp recording to any class."""
import datetime
def stamp(self):
import datetime
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
class UserService(LogMixin, TimestampMixin):
def create_user(self, name):
self.log(f"Creating user: {name} at {self.stamp()}")
return {"name": name}
svc = UserService()
user = svc.create_user("Alice")
Output (timestamp will vary):
[LOG] UserService: Creating user: Alice at 2024-03-15 10:22:47
Mixins give you modular, reusable behaviour without creating deep inheritance hierarchies.
| Pitfall | Why it Happens | How to Avoid |
|---|---|---|
| Unexpected method shadowing | Two parents define the same method name | Inspect __mro__ to understand which one wins |
__init__ not called | Parent __init__ silently skipped in the chain | Use cooperative super().__init__(**kwargs) — see next lesson |
| Too many parents | Deep, tangled hierarchies become unreadable | Limit to one real parent + mixins |
| Tight coupling | Subclass depends on internals of multiple parents | Prefer composition for complex dependencies |
TypeError: Cannot create a consistent MRO | Classes listed in an order that violates C3 rules | Reorder the base classes in the class definition |
class Child(A, B): syntax.ClassName.__mro__ to inspect exactly which order Python will search for methods.Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises