Mixin — the inheritance pattern that doesn't pretend to be is-a
A mixin is a class designed to be combined with others, never instantiated alone. It typically provides a focused chunk of behavior — SerializableMixin adds to_json(), LoggingMixin adds log(). The receiving class inherits from the mixin to get that behavior. The naming convention helps: NameMixin reads like a label, not a noun.
Why mixins exist when composition is preferred
If composition is the recommended default, why mixins? They earn their keep when (a) the behavior really should be on every method call, (b) the behavior depends on the host class's attributes (a mixin can use self.x from whatever class includes it), and (c) you need polymorphic dispatch — code that works with any "serializable" object regardless of concrete class.
The risks — diamonds and surprise overrides
Two mixins that define the same method create a diamond. Adding a mixin to a hierarchy can override methods you didn't expect to override. The MRO determines which wins. Use mixins for orthogonal concerns where collisions are unlikely, not for overlapping ones.
When composition wins
Most of the time. self.serializer = JsonSerializer() is clearer than class MyClass(JsonSerializerMixin):. You can swap serializers without changing the class. The hierarchy stays flat. Tests can inject fakes. The cost is one indirection (obj.serializer.serialize() vs obj.serialize()) — usually worth it.
Pythonic Way: Reach for mixins when (1) the behavior is genuinely cross-cutting (every method on the class should see it), (2) the host class won't conflict with the mixin's methods, (3) you want isinstance(obj, SerializableMixin) to mean something. Otherwise reach for composition.
Code
Mixin — adds focused behavior·python
import json
class JsonMixin:
def to_json(self):
return json.dumps(self.__dict__)
@classmethod
def from_json(cls, s):
return cls(**json.loads(s))
class User(JsonMixin):
def __init__(self, name, email):
self.name = name
self.email = email
u = User("alice", "a@x.com")
print(u.to_json()) # {"name": "alice", "email": "a@x.com"}
u2 = User.from_json(u.to_json())
print(u2.name, u2.email)
# JsonMixin can be reused for any class with simple attributes
import json
class JsonSerializer:
def serialize(self, obj):
return json.dumps(obj.__dict__)
def deserialize(self, cls, s):
return cls(**json.loads(s))
class User:
serializer = JsonSerializer() # composition
def __init__(self, name, email):
self.name = name
self.email = email
u = User("alice", "a@x.com")
print(User.serializer.serialize(u))
# Easy to swap: User.serializer = XmlSerializer()
Mixin gone wrong — name collision·python
class LoggerMixin:
def log(self, msg):
print(f"[LOG] {msg}")
class MetricsMixin:
def log(self, msg): # SAME name — collision
print(f"[METRIC] {msg}")
class Service(LoggerMixin, MetricsMixin):
pass
s = Service()
s.log("hello") # [LOG] hello — LoggerMixin wins (first in MRO)
# Refactoring tip: rename one to avoid the collision
# class LoggerMixin: def log_event(self, ...)
# class MetricsMixin: def emit_metric(self, ...)
Define EqualityMixin that provides __eq__ (compares all instance attributes) and __hash__ (frozenset of items). Create three unrelated classes (Coordinate(x, y), Color(r, g, b), Tag(name)) all using EqualityMixin. Demonstrate that equality works correctly across all three. Then write a composition-based version: a Comparator helper class with the same logic, and have each class hold a comparator attribute. Compare which one feels cleaner.
Progress
Progress is local-only — sign in to sync across devices.