Mixins & Multiple Inheritance
Reviewed & published by Brayan K
Learn the REAL Python OOP system used by Django, FastAPI, SQLAlchemy, Pydantic, PyTorch, and Scrapy. Master mixins, multiple inheritance, and professional OOP patterns used in large-scale frameworks.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
🔥 Mixins, Multiple Inheritance & OOP Patterns
This lesson teaches the REAL Python OOP system used by:
| Framework | Common Mixins | Purpose |
|---|---|---|
| Django | LoginRequiredMixin, PermissionMixin | Authentication & authorization |
| SQLAlchemy | TimestampMixin, SerializerMixin | Database models & serialization |
| FastAPI | ModelConfigMixin | Configuration & validation |
| Scrapy | RetryMixin, LogMixin | Robustness & debugging |
This is how large Python frameworks organise reusable behaviour at scale.
⭐ 1. What Are Mixins?
- ✔ a small class
- ✔ provides one specific behaviour
- ✔ not meant to be instantiated on its own
- ✔ used to "mix" extra features into other classes
Example of a simple mixin:
class PrintableMixin:
def pretty(self):
return f"<{self.__class__.__name__}: {self.__dict__}>"
class User(PrintableMixin):
def __init__(self, name):
self.name = name
# Now:
print(User("Boopie").pretty())
# ✅ Expected output:
# <User: {'name': 'Boopie'}>Mixins = adding reusable abilities, not designing base types.
⭐ 2. Why Mixins Exist
Large codebases avoid copying logic.
Instead, they break features into "behaviour modules":
- ✔ LoggingMixin
- ✔ PermissionMixin
- ✔ RateLimitMixin
- ✔ CacheMixin
- ✔ TokenAuthMixin
- ✔ SerializerMixin
- ✔ RetryMixin
- ✔ EventEmitterMixin
Each mixin provides ONE capability.
Your class can combine many of them:
# Example of combining multiple mixins
class AuthMixin:
def authenticate(self):
print("Authenticating...")
class LoggingMixin:
def log(self, msg):
print(f"[LOG] {msg}")
class RetryMixin:
def retry(self, func, attempts=3):
for i in range(attempts):
try:
return func()
except Exception as e:
print(f"Attempt {i+1} failed: {e}")
class CacheMixin:
_cache = {}
def cache_get(self, key):
return self._cache.get(key)
class APIClient(AuthMixin, LoggingMixin, RetryMixin, CacheMixin):
def fetch_data(self):
self.authenticate()
self.log("Fetching data...")
return "Data fetched!"
client = APIClient()
print(client.fetch_data())
# ✅ Expected output:
# Authenticating...
# [LOG] Fetching data...
# Data fetched!This avoids the inheritance mess of shoving everything into one giant parent class.
⭐ 3. When NOT To Use Mixins
Do NOT use mixins for:
- ❌ complex logic
- ❌ core business functionality
- ❌ storage of critical state
- ❌ large method sets
- ❌ tightly coupled behaviour
- ✔ convenience methods
- ✔ add-on features
- ✔ small utilities
⚙️ 4. Multiple Inheritance — How Python Makes This Work
Python supports full multiple inheritance:
class A:
def greet(self):
return "Hello from A"
class B:
def greet(self):
return "Hello from B"
class C(A, B):
pass
# C inherits from both A and B
c = C()
print(c.greet()) # prints "Hello from A" because A is first
# ✅ Expected output:
# Hello from ABehind the scenes, Python uses:
👉 MRO (Method Resolution Order)
This determines which class Python checks first when calling a method.
⭐ 5. How to View the MRO
Critical for debugging inheritance:
class A:
def speak(self): print("A")
class B:
def speak(self): print("B")
class C(A, B):
pass
# View the Method Resolution Order
print(C.mro())
# Or use __mro__
print(C.__mro__)
# Output example:
# [<class C>, <class A>, <class B>, <class object>]
# ✅ Expected output:
# [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>]
# (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)- ✔ method lookup
- ✔ attribute access
- ✔ super() behaviour
- ✔ complex inheritance chains
- ✔ how Python avoids the diamond problem
🔷 6. The Diamond Problem (and Python's solution)
All classes inherit from A.
Python solves this using C3 linearization.
- ✔ deterministic lookup order
- ✔ no ambiguity
- ✔ super() always moves to the next class in the hierarchy
⭐ 7. Why super() Matters in Multiple Inheritance
Many languages treat super() like:
➡️ "call ONLY the parent class"
Python treats super() as:
➡️ "call the NEXT class in the MRO chain"
This allows cooperative multiple inheritance.
class A:
def process(self):
print("A")
class B:
def process(self):
print("B")
class C:
def process(self):
print("C")
class D(A, B, C):
def process(self):
print("D")
super().process()
# Watch the chain work!
d = D()
d.process()
print("\nMRO:", [cls.__name__ for cls in D.mro()])
# ✅ Expected output:
# D
# A
#
# MRO: ['D', 'A', 'B', 'C', 'object']That's the power of super() + MRO.
⚡ 8. Designing Cooperative Mixins
Every mixin you write should:
- ✔ call super()
- ✔ accept *args, **kwargs
- ✔ avoid breaking the chain
class LoggingMixin:
def save(self, *args, **kwargs):
print("Logging save...")
return super().save(*args, **kwargs)
class ValidationMixin:
def save(self, *args, **kwargs):
print("Validating...")
return super().save(*args, **kwargs)
class BaseModel:
def save(self, *args, **kwargs):
print("Saving to database!")
return True
class User(LoggingMixin, ValidationMixin, BaseModel):
pass
user = User()
user.save()
# ✅ Expected output:
# Logging save...
# Validating...
# Saving to database!If every class cooperates, Python can thread all behaviours together.
This is how Django CBVs (Class-Based Views) work internally.
🟢 Worked Example: a save() that three classes take part in
This is the pattern the whole lesson has been building towards, on one runnable program. Three classes each define save(). You call it once. Watch the order the messages come out in, and compare that order to the MRO printed at the top — they are the same list, and that is not a coincidence.
# 🟢 WORKED EXAMPLE — one save() call, three classes cooperating
class BaseModel:
"""The real work lives here. Everything above it eventually calls into this."""
def __init__(self, **fields):
self.fields = fields
def save(self):
print("BaseModel.save -> writing to the database:", self.fields)
return True # the end of the chain: no super() call, it does the job
class TimestampMixin:
def save(self):
self.fields["updated"] = "2026-05-01T09:00:00"
print("TimestampMixin -> stamped updated time")
return super().save() # hand on to the NEXT class in the MRO, not to a parent
class ValidationMixin:
def save(self):
if not self.fields.get("name"):
raise ValueError("name is required")
print("ValidationMixin -> fields look valid")
return super().save() # only continues the chain if validation passed
# Left to right = first to run. Validation guards the door, timestamp fills in,
# BaseModel does the actual saving.
class User(ValidationMixin, TimestampMixin, BaseModel):
pass
print("MRO:", [cls.__name__ for cls in User.__mro__])
print()
user = User(name="Ada")
print("save() returned:", user.save())
print("fields now :", user.fields)
print()
# Now the interesting half: what happens when a link in the chain refuses to continue?
broken = User(name="")
try:
broken.save()
except ValueError as err:
print("ValueError:", err)
print("did BaseModel.save run? no — the chain stopped at ValidationMixin")
# ✅ Expected output:
# MRO: ['User', 'ValidationMixin', 'TimestampMixin', 'BaseModel', 'object']
#
# ValidationMixin -> fields look valid
# TimestampMixin -> stamped updated time
# BaseModel.save -> writing to the database: {'name': 'Ada', 'updated': '2026-05-01T09:00:00'}
# save() returned: True
# fields now : {'name': 'Ada', 'updated': '2026-05-01T09:00:00'}
#
# ValueError: name is required
# did BaseModel.save run? no — the chain stopped at ValidationMixinThe thing to take away: super() does not mean "my parent". It means "the next class along the MRO of the object I am actually running on". TimestampMixin has no parent with a save() at all — it works only because User's MRO puts BaseModel after it. That is why a mixin that forgets to call super() silently kills every behaviour behind it in the chain.
🎯 Your Turn: stack two decorating mixins
Three blanks. Two of them are about the order you list the base classes in — which is the part that actually decides what your program does.
# 🎯 YOUR TURN — fill in the blanks marked with ___
class Report:
def render(self):
return "report body" # the plain text, end of the chain
class UppercaseMixin:
def render(self):
# 1) Get the text from the next class along, then shout it.
return ___.render().upper() # 👉 which built-in call continues the chain?
class BorderMixin:
def render(self):
return f"*** {super().render()} ***"
# 2 & 3) Order decides the result. You want the border drawn around text that has
# ALREADY been uppercased — so the border must run FIRST and go leftmost.
class FancyReport(___, ___, Report): # 👉 two class names, in the right order
pass
print(FancyReport().render())
print([c.__name__ for c in FancyReport.__mro__])
# ✅ Expected output:
# *** REPORT BODY ***
# ['FancyReport', 'BorderMixin', 'UppercaseMixin', 'Report', 'object']Swap the two mixins round and run it again. You still get *** REPORT BODY ***, because upper() on a string containing asterisks changes nothing — a useful reminder that "it printed something" is not the same as "the order was right". Change UppercaseMixin to lowercase the text and the difference becomes obvious.
🔶 9. Mixins Should NOT Have __init__
Mixins generally avoid __init__ because it breaks cooperative inheritance.
If needed, you MUST write:
class ConfigMixin:
def __init__(self, *args, **kwargs):
self.debug = True
super().__init__(*args, **kwargs)
class BaseClass:
def __init__(self, name):
self.name = name
class MyClass(ConfigMixin, BaseClass):
pass
obj = MyClass("Test")
print(f"Name: {obj.name}, Debug: {obj.debug}")
# ✅ Expected output:
# Name: Test, Debug: TrueOtherwise, your mixin may block other parents from initialising.
⭐ 10. Real-World Mixin Examples
Examples from popular frameworks:
# Django-style mixins
class LoginRequiredMixin:
"""Requires user to be logged in"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return "Redirect to login"
return super().dispatch(request, *args, **kwargs)
class PermissionRequiredMixin:
"""Requires specific permissions"""
permission_required = None
def has_permission(self):
return self.permission_required is not None
# SQLAlchemy-style mixin
class TimestampMixin:
"""Adds created_at and updated_at fields"""
created_at = "Column(DateTime, default=now)"
updated_at = "Column(DateTime, onupdate=now)"
# FastAPI/Pydantic-style mixin
class ModelConfigMixin:
class Config:
orm_mode = True
print("Mixins dominate real frameworks!")
print("Django, SQLAlchemy, FastAPI all use this pattern.")
# ✅ Expected output:
# Mixins dominate real frameworks!
# Django, SQLAlchemy, FastAPI all use this pattern.Mixins dominate real frameworks.
🧠 11. Interface Mixins (Behaviour Contracts)
You can define behaviour expectations:
class JSONSerializableMixin:
def to_json(self):
raise NotImplementedError("Subclass must implement to_json()")
class XMLSerializableMixin:
def to_xml(self):
raise NotImplementedError("Subclass must implement to_xml()")
class User(JSONSerializableMixin):
def __init__(self, name, age):
self.name = name
self.age = age
def to_json(self):
return f'{{"name": "{self.name}", "age": {self.age}}}'
user = User("Boopie", 16)
print(user.to_json())
# ✅ Expected output:
# {"name": "Boopie", "age": 16}- ✔ you need consistency
- ✔ library expects a method to exist
- ✔ plugin systems
🔥 12. Multiple Inheritance for Role Composition
Rather than long inheritance like:
AdminUser → User → BaseUser → Object
You use role stacking:
class AuthMixin:
def authenticate(self):
return True
class PermissionMixin:
permissions = []
def has_permission(self, perm):
return perm in self.permissions
class LoggingMixin:
def log_action(self, action):
print(f"[LOG] {action}")
class BaseUser:
def __init__(self, name):
self.name = name
class AdminUser(AuthMixin, PermissionMixin, LoggingMixin, BaseUser):
permissions = ["read", "write", "delete", "admin"]
admin = AdminUser("SuperAdmin")
admin.log_action("Created new user")
print(f"Has admin permission: {admin.has_permission('admin')}")
# ✅ Expected output:
# [LOG] Created new user
# Has admin permission: TrueThis is scalable for 10+ behaviour components.
🧩 13. Composition vs Inheritance (Critical Design Decision)
- • "is a" relationship
- • class hierarchy makes sense
- • polymorphic behaviour
- • "has a" relationship
- • behaviour shouldn't leak
- • state belongs to specific objects
❌ BAD: Dog inherits from Engine
✔ GOOD: Car has an Engine object
🧱 14. Pattern: Behaviour Trees With Mixins
Create classes with distinct abilities:
class MoveMixin:
def move(self, direction):
print(f"Moving {direction}")
class AttackMixin:
def attack(self, target):
print(f"Attacking {target}!")
class HealMixin:
def heal(self, amount):
print(f"Healing for {amount} HP")
# Compose characters:
class Warrior(MoveMixin, AttackMixin):
pass
class Mage(MoveMixin, HealMixin, AttackMixin):
pass
class Healer(MoveMixin, HealMixin):
pass
# Test the characters
warrior = Warrior()
warrior.move("forward")
warrior.attack("enemy")
mage = Mage()
mage.heal(50)
mage.attack("boss")
# ✅ Expected output:
# Moving forward
# Attacking enemy!
# Healing for 50 HP
# Attacking boss!This is used in:
- ✔ simulations
🧵 15. Pattern: Mechanical Mixins (Deep Automation)
Advanced mixins generate code:
class AutoReprMixin:
def __repr__(self):
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{self.__class__.__name__}({attrs})"
class AutoEqMixin:
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return self.__dict__ == other.__dict__
# Plug them in:
class User(AutoReprMixin, AutoEqMixin):
def __init__(self, name, age):
self.name = name
self.age = age
u1 = User("Boopie", 16)
u2 = User("Boopie", 16)
u3 = User("Josh", 25)
print(u1)
print(f"u1 == u2: {u1 == u2}")
print(f"u1 == u3: {u1 == u3}")
# ✅ Expected output:
# User(name='Boopie', age=16)
# u1 == u2: True
# u1 == u3: False🏁 Mini-Challenge: write two mixins from scratch
No blanks. Write two small mixins and one class that uses both — and notice while you do it that the second mixin depends on the first one's method rather than on any shared parent. That loose contract is exactly how Django's view mixins are built.
# 🏁 MINI-CHALLENGE — write it yourself
import json
# 1. class ToDictMixin:
# to_dict(self) -> a plain dict copy of the instance attributes.
# Every object keeps them in self.__dict__, so dict(self.__dict__) is the whole job.
#
# 2. class JSONMixin:
# to_json(self) -> json.dumps(self.to_dict(), sort_keys=True)
# Note it calls self.to_dict() — a method JSONMixin does NOT define. It trusts
# that whatever class it is mixed into also has ToDictMixin. That is the contract.
#
# 3. class Product(JSONMixin, ToDictMixin):
# __init__(self, name, pence) storing both as attributes.
# your code here
# ---- test harness: leave this at the bottom ----
p = Product("tea", 120)
print(p.to_dict())
print(p.to_json())
print([c.__name__ for c in Product.__mro__])
print(isinstance(p, ToDictMixin))
# ✅ Expected output:
# {'name': 'tea', 'pence': 120}
# {"name": "tea", "pence": 120}
# ['Product', 'JSONMixin', 'ToDictMixin', 'object']
# True
#
# Look closely at the first two lines: the dict prints with single quotes because that
# is Python's repr, while the JSON prints with double quotes because that is what the
# JSON format demands. Same data, two different notations.📋 Quick Reference — Mixins & Multiple Inheritance
| Concept | What it does |
|---|---|
| class C(A, B): | Multiple inheritance — inherits from A and B |
| C.__mro__ | Method Resolution Order for lookups |
| super() | Call next class in MRO chain |
| class LogMixin: | Reusable behaviour added via inheritance |
| isinstance(obj, Mixin) | Check if mixin is applied |
🎉 Great work! You've completed this lesson.
You can now compose reusable behaviours with mixins and reason about MRO — the same pattern Django's class-based views use.
Practice quiz
What best describes a mixin?
- A large base class meant to be instantiated directly
- A function that returns a class
- A small class that provides one specific reusable behaviour, not meant to stand alone
- A module of unrelated helper functions
Answer: A small class that provides one specific reusable behaviour, not meant to stand alone. A mixin is a small class adding one capability that you mix into other classes; it is not instantiated on its own.
For 'class C(A, B): pass' where both A and B define greet(), which greet() runs for C()?
- A.greet, because A comes first in the MRO
- B.greet, because B is listed last
- Neither; it raises an error
- Both run in sequence
Answer: A.greet, because A comes first in the MRO. Python follows the MRO left to right, so A (listed first) is found before B.
What does C.__mro__ (or C.mro()) give you?
- The class's instance attributes
- A list of mixins only
- The class's metaclass
- The Method Resolution Order — the linear order Python searches for methods
Answer: The Method Resolution Order — the linear order Python searches for methods. __mro__ is the ordered tuple of classes Python checks when resolving attributes and methods.
What does Python use to compute a deterministic MRO and solve the diamond problem?
- Random ordering
- C3 linearization
- Depth-first left-to-right only
- Alphabetical ordering of class names
Answer: C3 linearization. CPython uses C3 linearization to produce a consistent, unambiguous MRO.
In multiple inheritance, what does super() actually do?
- Calls the NEXT class in the MRO chain
- Always calls the single direct parent class
- Calls the object base class directly
- Skips all parent classes
Answer: Calls the NEXT class in the MRO chain. super() delegates to the next class in the MRO, which is what makes cooperative inheritance work.
For 'class D(A, B, C):' where D.process calls super().process(), what is D's MRO order?
- D, C, B, A, object
- A, B, C, D, object
- D, A, B, C, object
- D, object, A, B, C
Answer: D, A, B, C, object. The MRO is D, A, B, C, object — left to right across the bases, then object.
For a cooperative mixin to not break the chain, its overriding method should:
- Never call super()
- Call super() and accept *args, **kwargs
- Return None immediately
- Define its own __init__ that ignores parents
Answer: Call super() and accept *args, **kwargs. Cooperative mixins call super() and pass through *args/**kwargs so every class in the MRO runs.
Why do mixins generally avoid defining __init__?
- Because __init__ is illegal in mixins
- Because mixins cannot store data ever
- Because __init__ makes them abstract
- Because it can break cooperative initialization across the MRO
Answer: Because it can break cooperative initialization across the MRO. A poorly written mixin __init__ can block other parents from initializing; if needed it must call super().__init__.
In 'class User(LoggingMixin, ValidationMixin, BaseModel)', calling user.save() that chains super() prints in what order?
- Saving to database!, Validating..., Logging save...
- Logging save..., Validating..., Saving to database!
- Validating..., Logging save..., Saving to database!
- Only Saving to database!
Answer: Logging save..., Validating..., Saving to database!. Following the MRO, LoggingMixin runs first, then ValidationMixin, then BaseModel does the real work.
When is composition ("has a") generally preferred over inheritance ("is a")?
- When there is a clear is-a relationship
- Whenever you need polymorphism
- When behaviour belongs to a specific object and shouldn't leak into a hierarchy
- Never; inheritance is always better
Answer: When behaviour belongs to a specific object and shouldn't leak into a hierarchy. Composition suits has-a relationships where state/behaviour belongs to a contained object rather than a subtype.
Continue this course
- Previous: Operator Overloading & Custom Behaviours
- Next: Design Patterns in Python (Singleton, Factory, Strategy) — Apply classic GoF patterns idiomatically in Python
- Quick reference: Python cheat sheet