Why super() doesn't mean "parent class" (and what it really does)
Timothy stared at his screen in complete bewilderment. He'd written what seemed like straightforward inheritance code, but the output made absolutely no sense.
class A:
def process(self):
print("A.process()")
class B(A):
def process(self):
print("B.process()")
super().process()
class C(A):
def process(self):
print("C.process()")
super().process()
class D(B, C):
def process(self):
print("D.process()")
super().process()
d = D()
d.process()
He expected the output to be:
D.process()
B.process()
A.process()
After all, D inherits from B, and B inherits from A. That's how inheritance works, right?
But when he ran the code, he got:
D.process()
B.process()
C.process()
A.process()
"What?!" Timothy exclaimed, catching Margaret's attention from across the library. "Why is C.process() being called? Class B doesn't inherit from C! When B calls super().process(), it should call A.process(), not C.process()!"
Margaret walked over, a knowing smile on her face. "Ah, you've discovered one of Python's most misunderstood features. You think super() means 'call my parent class,' don't you?"
"Of course it does! super() calls the superclass—the parent!"
"That," Margaret said gently, "is precisely what everyone thinks. And it's precisely wrong. super() doesn't call your parent class, Timothy. It calls the next class in the Method Resolution Order."
"The... what?"
"Come," Margaret said, opening a thick tome titled The Method Resolution Order: Python's Hidden Path. "Let me show you how Python really resolves methods."
The Method Resolution Order (MRO)
"Every class in Python," Margaret began, "has an attribute called __mro__—the Method Resolution Order. It's a tuple that defines the exact sequence Python follows when looking up attributes."
She typed:
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
print(D.__mro__)
Output:
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>,
<class '__main__.A'>, <class 'object'>)
"This tuple," Margaret explained, "is the order Python searches for methods and attributes. When you access d.process(), Python looks:
- First in
D - Then in
B - Then in
C - Then in
A - Finally in
object(the root of all classes)
And here's the crucial part: super() doesn't mean 'my parent class'—it means 'the next class in the MRO.'"
"The MRO also affects other operations," Margaret added. "For instance, isinstance() and issubclass() walk the MRO:"
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
obj = D()
# isinstance() checks if the object's class is in the MRO
print(isinstance(obj, D)) # True - D is in D's MRO
print(isinstance(obj, B)) # True - B is in D's MRO
print(isinstance(obj, C)) # True - C is in D's MRO
print(isinstance(obj, A)) # True - A is in D's MRO
print(isinstance(obj, object)) # True - object is always in MRO
# issubclass() checks if one class appears after another in the MRO
print(issubclass(D, B)) # True
print(issubclass(D, C)) # True
print(issubclass(D, A)) # True
# The MRO makes these checks simple and efficient
print(D.__mro__)
# All the classes where isinstance returns True!
Timothy studied the MRO carefully. "So when B.process() calls super().process(), it doesn't look at B's parent class A. It looks at the next class in the MRO after B, which is... C!"
"Exactly! Let's trace through your original example step by step:"
class A:
def process(self):
print("A.process()")
class B(A):
def process(self):
print("B.process()")
super().process() # Next in MRO after B
class C(A):
def process(self):
print("C.process()")
super().process() # Next in MRO after C
class D(B, C):
def process(self):
print("D.process()")
super().process() # Next in MRO after D
# The MRO is: D -> B -> C -> A -> object
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
d = D()
d.process()
Margaret traced the execution:
1. d.process() starts in D
- Prints "D.process()"
- Calls super().process() → next in MRO is B
2. B.process() executes
- Prints "B.process()"
- Calls super().process() → next in MRO is C (not A!)
3. C.process() executes
- Prints "C.process()"
- Calls super().process() → next in MRO is A
4. A.process() executes
- Prints "A.process()"
- No super() call, done
"The key insight," Margaret emphasized, "is that super() in B doesn't know or care that B inherits from A. It only knows the MRO of the instance being operated on—in this case, an instance of D."
Timothy's eyes widened. "So the same method B.process() will call different next methods depending on what class the instance actually is?"
"Precisely! Watch:"
# Same classes as before
# Instance of B directly
b = B()
print("MRO of B:", B.__mro__)
# (<class 'B'>, <class 'A'>, <class 'object'>)
b.process()
# B.process()
# A.process()
# Instance of D
d = D()
print("\nMRO of D:", D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
d.process()
# D.process()
# B.process()
# C.process()
# A.process()
"When you call b.process() on a B instance, the MRO is B -> A -> object, so super() in B calls A. But when you call d.process() on a D instance, the MRO is D -> B -> C -> A -> object, so super() in B calls C!"
How Python Builds the MRO: C3 Linearization
"But how does Python determine this order?" Timothy asked. "Why is the MRO D -> B -> C -> A and not some other sequence?"
Margaret pulled out a diagram. "Python uses an algorithm called C3 Linearization (also called C3 superclass linearization). It has three main rules:
- Children before parents (local precedence order)
- Left-to-right ordering (from the inheritance list)
- Monotonicity (preserve order from parent MROs)
The monotonicity rule is crucial: if class A appears before class B in any parent's MRO, then A must appear before B in the child's MRO (unless explicitly overridden in the child's direct inheritance list).
Let me show you how it builds the MRO for class D(B, C):"
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
Margaret wrote out the algorithm:
Step 1: Start with the class itself
L(D) = D + merge(L(B), L(C), [B, C])
Step 2: Expand the parent linearizations
L(B) = B, A, object
L(C) = C, A, object
L(D) = D + merge([B, A, object], [C, A, object], [B, C])
Step 3: Apply merge algorithm
- Take the first head that doesn't appear in any other tail
- "Head" = first element of a list
- "Tail" = everything after the first element
merge([B, A, object], [C, A, object], [B, C]):
First heads: B, C, B
B is not in any tail → take B
D, B + merge([A, object], [C, A, object], [C]):
First heads: A, C, C
A is in tail of [C, A, object] → skip
C is not in any tail → take C
D, B, C + merge([A, object], [A, object]):
First heads: A, A
A is not in any tail → take A
D, B, C, A + merge([object], [object]):
First head: object
object is not in any tail → take object
Final: D, B, C, A, object
"This algorithm ensures several important properties," Margaret explained:
# Property 1: A class always comes before its parents
class Parent:
pass
class Child(Parent):
pass
print(Child.__mro__)
# (<class 'Child'>, <class 'Parent'>, <class 'object'>)
# Child before Parent ✓
# Property 2: Parents are in the order specified
class A:
pass
class B:
pass
class C(A, B): # A before B
pass
print(C.__mro__)
# (<class 'C'>, <class 'A'>, <class 'B'>, <class 'object'>)
# A appears before B ✓
# Property 3: A class's MRO is consistent with its parents' MROs
class D(B, A): # B before A
pass
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'A'>, <class 'object'>)
"But what happens," Timothy asked, "if you create an inheritance hierarchy that violates these rules?"
Margaret smiled. "Excellent question. Python refuses to create such classes:"
class X:
pass
class Y(X):
pass
class Z(X, Y): # Try to put X before Y, but Y inherits from X
pass
# TypeError: Cannot create a consistent method resolution
# order (MRO) for bases X, Y
"The algorithm says 'X must come before Y' (from the Z(X, Y) declaration), but also 'Y must come before X' (because Y inherits from X). This is impossible, so Python raises an error."
"Here's another violation of monotonicity:"
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
# D's MRO: D -> B -> C -> A
# This respects that B.MRO has (B, A) and C.MRO has (C, A)
# But what if we try:
class E(C, B):
pass
# E's MRO: E -> C -> B -> A
# This also works! Order reversed but still consistent
# However, this breaks:
class F(A, B): # Try to put A before B, but B inherits from A
pass
# TypeError: Cannot create a consistent method resolution
# order (MRO) for bases A, B
#
# We're saying "A before B" but B's MRO says "B before A"
# Inconsistent!
"The C3 algorithm detects these inconsistencies and prevents you from creating impossible hierarchies."
The Diamond Problem
"Now let's look at why the MRO matters so much," Margaret said, drawing a diagram on her slate:
A
/ \
B C
\ /
D
"This is called the diamond problem. Class D inherits from both B and C, which both inherit from A. The question is: when D calls a method that all classes implement, what order are they called in?"
class A:
def __init__(self):
print("A.__init__")
class B(A):
def __init__(self):
print("B.__init__")
super().__init__()
class C(A):
def __init__(self):
print("C.__init__")
super().__init__()
class D(B, C):
def __init__(self):
print("D.__init__")
super().__init__()
d = D()
# D.__init__
# B.__init__
# C.__init__
# A.__init__
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
"Notice something crucial," Margaret pointed out. "Even though both B and C inherit from A, A.__init__ is only called once. This is by design. The MRO ensures that every class appears exactly once, and super() ensures we follow the MRO correctly."
"What if we don't use super()?" Timothy asked.
class A:
def __init__(self):
print("A.__init__")
class B(A):
def __init__(self):
print("B.__init__")
A.__init__(self) # Direct call instead of super()
class C(A):
def __init__(self):
print("C.__init__")
A.__init__(self) # Direct call instead of super()
class D(B, C):
def __init__(self):
print("D.__init__")
B.__init__(self)
C.__init__(self)
d = D()
# D.__init__
# B.__init__
# A.__init__
# C.__init__
# A.__init__ ← Called twice!
"Disaster!" Margaret exclaimed. "A.__init__ is called twice. This might seem harmless in this example, but imagine if A was opening a file, creating a database connection, or allocating resources. You'd have duplicated resources and potential corruption."
"The MRO plus super() solves this by ensuring each class is called exactly once, in a predictable order."
Cooperative Multiple Inheritance
"The real power of super() and the MRO," Margaret explained, "emerges when you design classes to work cooperatively. This requires a specific pattern."
She wrote out an example:
class LoggingMixin:
def __init__(self, **kwargs):
print(f"LoggingMixin.__init__")
super().__init__(**kwargs) # Pass remaining kwargs along
class ValidationMixin:
def __init__(self, **kwargs):
print(f"ValidationMixin.__init__")
super().__init__(**kwargs)
class Base:
def __init__(self, name):
print(f"Base.__init__: name={name}")
self.name = name
class Document(LoggingMixin, ValidationMixin, Base):
def __init__(self, name, **kwargs):
print(f"Document.__init__")
super().__init__(name=name, **kwargs)
doc = Document("report.pdf", extra="value")
# Document.__init__
# LoggingMixin.__init__
# ValidationMixin.__init__
# Base.__init__: name=report.pdf
print(Document.__mro__)
# (<class 'Document'>, <class 'LoggingMixin'>,
# <class 'ValidationMixin'>, <class 'Base'>, <class 'object'>)
"Notice several critical patterns here," Margaret noted:
Pattern 1: Accept and pass **kwargs
"Every class except the final base class accepts **kwargs and passes them to super(). This allows arguments to flow through the entire MRO chain."
Pattern 2: Extract only what you need
class TimestampMixin:
def __init__(self, timestamp=None, **kwargs):
self.timestamp = timestamp or datetime.now()
super().__init__(**kwargs) # Pass the rest along
class AuthorMixin:
def __init__(self, author=None, **kwargs):
self.author = author or "Unknown"
super().__init__(**kwargs)
class Base:
def __init__(self, name):
self.name = name
class Article(TimestampMixin, AuthorMixin, Base):
def __init__(self, name, **kwargs):
super().__init__(name=name, **kwargs)
article = Article(
name="Python Deep Dive",
author="Margaret",
timestamp=datetime(2024, 1, 1)
)
print(f"{article.name} by {article.author} at {article.timestamp}")
"Each mixin extracts its parameters from **kwargs and passes the rest along. The chain continues until all parameters are consumed."
Pattern 3: Always call super()
"Every class in the chain must call super().__init__(), even if it doesn't think it has a parent class. Remember, super() follows the MRO of the instance, not the class hierarchy you see in the code."
class Mixin:
def __init__(self, **kwargs):
print("Mixin.__init__")
# Even though Mixin doesn't explicitly inherit from anything,
# we still call super() to continue the MRO chain
super().__init__(**kwargs)
class Base:
def __init__(self, value):
print(f"Base.__init__: value={value}")
self.value = value
class Combined(Mixin, Base):
def __init__(self, value, **kwargs):
super().__init__(value=value, **kwargs)
obj = Combined(value=42)
# Mixin.__init__
# Base.__init__: value=42
Timothy frowned. "But what if I mix classes that use super() with classes that don't?"
"Chaos," Margaret said flatly.
class A:
def __init__(self):
print("A.__init__")
class B(A):
def __init__(self):
print("B.__init__")
A.__init__(self) # Direct call - breaks the chain!
class C(A):
def __init__(self):
print("C.__init__")
super().__init__() # Uses super()
class D(B, C):
def __init__(self):
print("D.__init__")
super().__init__()
d = D()
# D.__init__
# B.__init__
# A.__init__ ← Chain stops here! C is never called
"Because B calls A directly instead of using super(), the MRO chain is broken. C.__init__ is never called, even though it's in the MRO. The rule is: either all classes in a hierarchy use super(), or none of them do."
super() Without Arguments (Python 3 Magic)
"You may have noticed," Margaret said, "that we've been writing super() without any arguments. In Python 2, you had to write super(CurrentClass, self). How does Python 3 know what class and instance to use?"
Timothy thought about it. "Some kind of... magic?"
"Close," Margaret smiled. "It's a clever use of Python's closure mechanism. When a class is created, Python binds a special closure variable called __class__ to any method that uses super()."
class Example:
def method(self):
print(super())
# Python transforms this to:
# print(super(__class__, self))
# where __class__ is bound at class creation time
# Let's peek at the closure
import inspect
print(Example.method.__code__.co_freevars)
# ('__class__',)
# The method has a closure that captures the class
print(Example.method.__closure__)
# (<cell at 0x...: type object at 0x...>,)
"When you call super() with no arguments, Python:
- Looks for
__class__in the function's closure (bound at class creation) - Gets
self(orclsfor classmethods) from the local scope - Effectively calls
super(__class__, self)
The __class__ reference is bound when the class is created, not at call time, which is why it works even with renamed classes:"
class Original:
def method(self):
return super()
# Rename the class
Renamed = Original
obj = Renamed()
print(obj.method())
# <super: <class 'Original'>, <Renamed object>>
# Still knows the original class name!
# Even if we delete the original name binding
Temp = Original
del Original
obj = Temp()
obj.method() # Still works! __class__ was bound at class creation
"But there are cases where you still need explicit arguments:"
class A:
def method(self):
return "A.method"
class B(A):
def method(self):
return "B.method"
class C(B):
def method(self):
return "C.method"
def skip_parent(self):
"""Skip B and go directly to A in the MRO"""
# Normal super() would call B.method
result_b = super().method() # Calls B.method
# But we can explicitly skip B by using super(B, self)
# This starts the search AFTER B in the MRO
result_a = super(B, self).method() # Calls A.method
return f"Normal: {result_b}, Skipped: {result_a}"
obj = C()
print(obj.skip_parent())
# Normal: B.method, Skipped: A.method
# Another case: calling super() outside a method
class D(A):
# This doesn't work - no __class__ closure
# external_super = super() # RuntimeError: super(): no arguments
def method(self):
# But this works - inside a method with __class__ closure
return super()
# Also useful: super() in a function that's assigned to a class later
def external_method(self):
# No __class__ closure here
# return super().method() # RuntimeError
# Must use explicit form
return super(D, self).method()
D.external = external_method
Real-World Pattern: Mixins
"Let's look at how real frameworks use multiple inheritance and the MRO," Margaret said, opening a Django models reference.
from datetime import datetime
# Mixin classes provide discrete functionality
class TimestampMixin:
"""Adds created_at and updated_at fields"""
def __init__(self, **kwargs):
self.created_at = datetime.now()
self.updated_at = datetime.now()
super().__init__(**kwargs)
def touch(self):
"""Update the updated_at timestamp"""
self.updated_at = datetime.now()
class SoftDeleteMixin:
"""Adds soft delete functionality"""
def __init__(self, **kwargs):
self.is_deleted = False
self.deleted_at = None
super().__init__(**kwargs)
def delete(self):
"""Soft delete - mark as deleted instead of removing"""
self.is_deleted = True
self.deleted_at = datetime.now()
def restore(self):
"""Restore a soft-deleted record"""
self.is_deleted = False
self.deleted_at = None
class AuditMixin:
"""Adds audit trail functionality"""
def __init__(self, **kwargs):
self.created_by = kwargs.pop('created_by', None)
self.updated_by = kwargs.pop('updated_by', None)
super().__init__(**kwargs)
def update_audit(self, user):
"""Update audit information"""
self.updated_by = user
if hasattr(self, 'touch'):
self.touch()
# Base model class
class Model:
"""Base class for all models"""
def __init__(self, id=None):
self.id = id
def save(self):
print(f"Saving {self.__class__.__name__} with id={self.id}")
# Combine mixins to create feature-rich models
class BlogPost(AuditMixin, TimestampMixin, SoftDeleteMixin, Model):
"""A blog post with full audit trail, timestamps, and soft delete"""
def __init__(self, id, title, content, **kwargs):
self.title = title
self.content = content
super().__init__(id=id, **kwargs)
# Create a blog post
post = BlogPost(
id=1,
title="Understanding super()",
content="It's all about the MRO!",
created_by="Margaret"
)
print(f"Post: {post.title}")
print(f"Created: {post.created_at}")
print(f"By: {post.created_by}")
print(f"Deleted: {post.is_deleted}")
# Use mixin functionality
post.delete()
print(f"After delete - Deleted: {post.is_deleted}")
post.restore()
print(f"After restore - Deleted: {post.is_deleted}")
# Check the MRO
print("\nMRO:")
for cls in BlogPost.__mro__:
print(f" {cls.__name__}")
# BlogPost
# AuditMixin
# TimestampMixin
# SoftDeleteMixin
# Model
# object
"This pattern is everywhere in Django," Margaret explained. "Mixins let you compose functionality without deep inheritance hierarchies. Each mixin is focused on one concern, and you combine them as needed."
Common Pitfalls and Debugging
"Let me show you the most common mistakes," Margaret said, opening a chapter titled "When MRO Goes Wrong."
Pitfall 1: Inconsistent Method Signatures
class A:
def process(self, data):
print(f"A: {data}")
class B(A):
def process(self, data, extra): # Different signature!
print(f"B: {data}, {extra}")
super().process(data)
class C(A):
def process(self, data):
print(f"C: {data}")
super().process(data)
class D(B, C):
def process(self, data, extra):
super().process(data, extra)
# d = D()
# d.process("test", "extra") # TypeError!
# C.process() doesn't accept 'extra' parameter
"Solution: Use **kwargs to handle varying signatures:"
class A:
def process(self, data, **kwargs):
print(f"A: {data}")
class B(A):
def process(self, data, extra=None, **kwargs):
print(f"B: {data}, {extra}")
super().process(data, **kwargs)
class C(A):
def process(self, data, **kwargs):
print(f"C: {data}")
super().process(data, **kwargs)
class D(B, C):
def process(self, data, extra=None, **kwargs):
super().process(data, extra=extra, **kwargs)
d = D()
d.process("test", extra="value")
# B: test, value
# C: test
# A: test
Pitfall 2: Calling super() in Some but Not All Methods
class Mixin1:
def __init__(self, **kwargs):
super().__init__(**kwargs) # Good - calls super()
self.mixin1_setup = True
class Mixin2:
def __init__(self, **kwargs):
# Bad - no super() call!
self.mixin2_setup = True
class Base:
def __init__(self):
self.base_setup = True
class Combined(Mixin1, Mixin2, Base):
def __init__(self):
super().__init__()
obj = Combined()
print(hasattr(obj, 'mixin1_setup')) # True
print(hasattr(obj, 'mixin2_setup')) # True
print(hasattr(obj, 'base_setup')) # False - Base.__init__ never called!
Pitfall 3: Assuming Parent Class Relationship
class Payment:
def process(self):
print("Processing payment...")
return True
class CreditCardPayment(Payment):
def process(self):
print("Validating credit card...")
# Assumption: super() will call Payment.process()
return super().process()
class SecurePayment:
def process(self):
print("Applying security checks...")
return True # Oops, doesn't call super()!
class SecureCreditCard(CreditCardPayment, SecurePayment):
pass
# What's the MRO?
print(SecureCreditCard.__mro__)
# SecureCreditCard -> CreditCardPayment -> SecurePayment -> Payment -> object
payment = SecureCreditCard()
result = payment.process()
# Validating credit card...
# Applying security checks...
# (Payment.process() is never called!)
"The problem is SecurePayment.process() doesn't call super(). The chain stops there."
Debugging Tool: Visualizing the MRO
def print_mro(cls, indent=0):
"""Print the MRO as a tree"""
print(" " * indent + cls.__name__)
def trace_super_calls(cls):
"""Show the complete super() chain for a class"""
print(f"super() chain for {cls.__name__}:")
for i, klass in enumerate(cls.__mro__):
print(f" {i}. {klass.__name__}")
if hasattr(klass, '__init__'):
# Check if __init__ calls super()
import inspect
source = inspect.getsource(klass.__init__)
has_super = 'super()' in source
print(f" __init__ calls super(): {has_super}")
# Example usage
class A:
def __init__(self):
super().__init__()
class B(A):
def __init__(self):
super().__init__()
class C(A):
def __init__(self):
super().__init__()
class D(B, C):
def __init__(self):
super().__init__()
trace_super_calls(D)
When NOT to Use Multiple Inheritance
"Multiple inheritance is powerful," Margaret cautioned, "but it's not always the right tool."
Use multiple inheritance when:
- You need to compose orthogonal behaviors (mixins)
- The mixins have no overlapping attributes
- All classes cooperate with
super() - The MRO makes intuitive sense
Avoid multiple inheritance when:
- Classes have conflicting method implementations
- You're inheriting from multiple concrete classes (not mixins)
- The inheritance hierarchy is unclear
- Composition would be simpler
# Instead of this:
class FileLogger:
def log(self, msg):
with open('log.txt', 'a') as f:
f.write(msg)
class ConsoleLogger:
def log(self, msg):
print(msg)
class DualLogger(FileLogger, ConsoleLogger): # Conflict!
pass
# Consider this:
class Logger:
def __init__(self, *loggers):
self.loggers = loggers
def log(self, msg):
for logger in self.loggers:
logger.log(msg)
file_logger = FileLogger()
console_logger = ConsoleLogger()
dual_logger = Logger(file_logger, console_logger)
"Composition often beats inheritance, even with super() and MRO."
Advanced Pattern: Abstract Base Classes with MRO
"One more advanced pattern," Margaret said, pulling out a final example. "Abstract base classes combined with mixins create powerful, type-safe designs:"
from abc import ABC, abstractmethod
class StorageBackend(ABC):
"""Abstract base class defining storage interface"""
@abstractmethod
def save(self, key, value):
pass
@abstractmethod
def load(self, key):
pass
# Cannot instantiate abstract class
try:
storage = StorageBackend()
except TypeError as e:
print(f"Error: {e}")
# Error: Can't instantiate abstract class StorageBackend with abstract methods load, save
class CompressionMixin:
"""Mixin that adds compression to storage"""
def save(self, key, value, **kwargs):
compressed = self._compress(value)
return super().save(key, compressed, **kwargs)
def load(self, key, **kwargs):
compressed = super().load(key, **kwargs)
return self._decompress(compressed)
def _compress(self, data):
return f"compressed({data})"
def _decompress(self, data):
return data.replace("compressed(", "").rstrip(")")
class EncryptionMixin:
"""Mixin that adds encryption to storage"""
def save(self, key, value, **kwargs):
encrypted = self._encrypt(value)
return super().save(key, encrypted, **kwargs)
def load(self, key, **kwargs):
encrypted = super().load(key, **kwargs)
return self._decrypt(encrypted)
def _encrypt(self, data):
return f"encrypted({data})"
def _decrypt(self, data):
return data.replace("encrypted(", "").rstrip(")")
class DictStorage(StorageBackend):
"""Concrete storage implementation using dict"""
def __init__(self):
self.storage = {}
def save(self, key, value):
print(f"DictStorage.save: {key} = {value}")
self.storage[key] = value
def load(self, key):
value = self.storage[key]
print(f"DictStorage.load: {key} = {value}")
return value
# Now DictStorage can be instantiated - it implements all abstract methods
storage = DictStorage() # Works!
# Compose features with mixins
class SecureStorage(EncryptionMixin, CompressionMixin, DictStorage):
"""Storage with both encryption and compression"""
pass
# Check the MRO
print("MRO:", [cls.__name__ for cls in SecureStorage.__mro__])
# ['SecureStorage', 'EncryptionMixin', 'CompressionMixin', 'DictStorage',
# 'StorageBackend', 'ABC', 'object']
# Use it
storage = SecureStorage()
storage.save("key1", "secret data")
# DictStorage.save: key1 = encrypted(compressed(secret data))
loaded = storage.load("key1")
# DictStorage.load: key1 = encrypted(compressed(secret data))
print(f"Loaded: {loaded}")
# Loaded: secret data
"Notice how the data flows through the MRO:
Saving:
-
EncryptionMixin.save()encrypts → callssuper() -
CompressionMixin.save()compresses → callssuper() -
DictStorage.save()stores the encrypted+compressed data
Loading:
-
DictStorage.load()retrieves encrypted+compressed data -
CompressionMixin.load()decompresses → returns to caller -
EncryptionMixin.load()decrypts → returns to caller
Each mixin adds its behavior in both directions. This is the power of cooperative multiple inheritance!"
Conclusion: The MRO Mastery
Timothy closed his notebook, his earlier confusion replaced by understanding. "So super() doesn't mean 'my parent class'—it means 'next in the MRO.' And the MRO is computed by C3 linearization to ensure a consistent, predictable order."
"Precisely," Margaret nodded. "Key principles to remember:
- Every class has an MRO determined by C3 linearization
-
super()follows the MRO of the instance, not the class hierarchy -
Always call
super()to continue the chain (in cooperative hierarchies) -
Use
**kwargsto pass arguments through the chain - Mixins should be orthogonal and focused on single concerns
-
Check the MRO with
cls.__mro__when debugging -
isinstance()andissubclass()also walk the MRO
The MRO enables Python's multiple inheritance to work reliably. Without it, diamond inheritance would be chaos. With it, you can compose complex behavior from simple mixins."
"One last thing," Margaret added. "super() also interacts with descriptors and properties in interesting ways. When you call super() from within a property getter, you need to be careful about recursion. But that's an advanced topic that builds on both the descriptor protocol and the MRO."
Timothy looked at his original confusing example with fresh eyes:
class D(B, C):
def process(self):
super().process() # Not "parent" - it's "next in D's MRO"
# When called on a D instance, the MRO is:
# D -> B -> C -> A -> object
# So super() in each class calls the NEXT one in THIS order
"It all makes sense now. The behavior isn't mysterious—it's following a clear, predictable algorithm."
"Exactly," Margaret smiled. "And now you understand one of Python's most sophisticated features. Speaking of sophisticated features," she added with a glint in her eye, "have you ever wondered how classes themselves are created? How class syntax actually works?"
Timothy's eyes widened. "Are you talking about... metaclasses?"
"Indeed, Timothy. But that," Margaret said, standing and returning the tome to its shelf, "is a story for another day. A much deeper dive into Python's object model."
"After multiple inheritance and the MRO," Timothy said with a grin, "I think I'm ready for anything."
Margaret laughed. "We shall see, Timothy. We shall see..."
Key Takeaways
-
super()calls the next class in the MRO, not the parent class - The MRO is computed using C3 linearization with consistent, predictable rules
- Every class appears exactly once in the MRO
-
Cooperative multiple inheritance requires calling
super()in every method -
Use
**kwargsto pass parameters through the inheritance chain - Mixins enable composition of orthogonal behaviors
-
Python 3's
super()automatically determines class and instance -
Debug MRO issues with
cls.__mro__andcls.mro() - Avoid multiple inheritance when composition is clearer
-
All classes in a cooperative hierarchy must consistently use
super()
Next in The Secret Life of Python: "Metaclasses: Classes That Make Classes"
Discussion Questions
- When is multiple inheritance appropriate vs. composition?
- How do popular frameworks (Django, Flask) use mixins?
- What are the performance implications of deep MRO chains?
- How does
super()work with properties and descriptors? - Can you create an MRO that violates the rules? What happens?
Share your MRO adventures and multiple inheritance patterns in the comments!
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (0)