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:

FrameworkCommon MixinsPurpose
DjangoLoginRequiredMixin, PermissionMixinAuthentication & authorization
SQLAlchemyTimestampMixin, SerializerMixinDatabase models & serialization
FastAPIModelConfigMixinConfiguration & validation
ScrapyRetryMixin, LogMixinRobustness & debugging

This is how large Python frameworks organise reusable behaviour at scale.

⭐ 1. What Are Mixins?

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":

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:

⚙️ 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 A

Behind 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'>)

🔷 6. The Diamond Problem (and Python's solution)

All classes inherit from A.

Python solves this using C3 linearization.

⭐ 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:

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 ValidationMixin

The 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: True

Otherwise, 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}

🔥 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: True

This is scalable for 10+ behaviour components.

🧩 13. Composition vs Inheritance (Critical Design Decision)

❌ 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:

🧵 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

ConceptWhat 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