Mixins and When Not to Use Inheritance
18 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Sometimes several unrelated classes need the same small behaviour, such as turning their data into a dictionary. A mixin shares that one behaviour without pretending the classes are one family.
A mixin adds one behaviour
Mixin is a small class that adds one extra behaviour and is not meant to be used on its own. It is listed as one of several bases of another class.
Product, Customer and Ticket have nothing in common except that each should describe itself as text. Write the behaviour once in a small class and list it as a base.
class DescribeMixin: def describe(self): fields = ", ".join(f"{k}={v}" for k, v in vars(self).items()) return f"{type(self).__name__}: {fields}"
class Product(DescribeMixin): def __init__(self, name, price): self.name = name self.price = priceExample 1 · One mixin, two unrelated classes
class DescribeMixin: def describe(self): fields = ", ".join(f"{k}={v}" for k, v in vars(self).items()) return f"{type(self).__name__}: {fields}"
class Product(DescribeMixin): def __init__(self, name, price): self.name = name self.price = price
class Customer(DescribeMixin): def __init__(self, name, city): self.name = name self.city = city
print(Product("Pen", 20).describe())print(Customer("Asha", "Pune").describe())Product: name=Pen, price=20 Customer: name=Asha, city=Pune
What this code does
vars(self)gives the attributes of whichever object calls it.- Product and Customer are not related, yet both gain
describe.
Example 2 · A mixin that returns a dictionary
class ToDictMixin: def to_dict(self): return dict(vars(self))
class Ticket(ToDictMixin): def __init__(self, ticket_id, status): self.ticket_id = ticket_id self.status = status
t = Ticket(7, "open")print(t.to_dict())print(isinstance(t, ToDictMixin)){'ticket_id': 7, 'status': 'open'}
TrueWhat this code does
dict(vars(self))copies the attributes into a new dictionary.- The mixin appears in
isinstance, but it is a helper, not a category.
The mixin has no __init__ and no data of its own. It only uses what the host class already has.
Put the mixin first
Python searches bases from left to right. A mixin that wraps a method must come before the main class, so its version runs first and calls super() to reach the rest.
class Record: def save(self): return ["saved"]
class AuditMixin: def save(self): steps = super().save() steps.append("audit logged") return steps
class Invoice(AuditMixin, Record): passExample 3 · Base order decides which method runs first
class Record: def save(self): return ["saved"]
class AuditMixin: def save(self): steps = super().save() steps.append("audit logged") return steps
class Invoice(AuditMixin, Record): pass
class Backwards(Record, AuditMixin): pass
print(Invoice().save())print([cls.__name__ for cls in Invoice.__mro__])print(Backwards().save())['saved', 'audit logged'] ['Invoice', 'AuditMixin', 'Record', 'object'] ['saved']
What this code does
- With the mixin first, its
saveruns andsuper()reaches Record. - With the mixin last, Record.save answers first and the audit step is skipped.
The Method Resolution Order (MRO) becomes Invoice, AuditMixin, Record, object. Reverse the bases and the audit step is never reached.
When a part is better than a mixin
A mixin works well when the behaviour needs no state of its own. If it needs its own data, or you want to swap it, make it a part and pass it in.
class Auditor: def __init__(self): self.log = []
def record(self, text): self.log.append(text)
class Invoice: def __init__(self, auditor): self.auditor = auditor
def save(self): self.auditor.record("invoice saved")Example 4 · The same feature as a part
class Auditor: def __init__(self): self.log = []
def record(self, text): self.log.append(text)
class Invoice: def __init__(self, auditor): self.auditor = auditor
def save(self): self.auditor.record("invoice saved")
class Payment: def __init__(self, auditor): self.auditor = auditor
def save(self): self.auditor.record("payment saved")
shared = Auditor()Invoice(shared).save()Payment(shared).save()print(shared.log)['invoice saved', 'payment saved']
What this code does
- Both classes report to one Auditor, so the log holds both events.
- A mixin would give each object its own copy of the behaviour and no shared log.
The Auditor keeps its own log, can be replaced by a fake, and is shared by many objects. A mixin could do none of that cleanly.
Signs that inheritance is the wrong tool
Stop and consider a part, a function or a mixin when any of these is true.
- The child would have to disable or ignore methods it inherits.
- You inherit only to reuse code, and the sentence "child is a kind of parent" sounds wrong.
- The hierarchy grows a new level for every small variation.
- Two parents need the same __init__ arguments and clash.
Next step
Check your understanding
10 quick questions