Fundamentals 8 min read

Understanding Python super() and Method Resolution Order (MRO) with Advanced Examples

This article explains how Python's super() works by invoking the next class in the Method Resolution Order (MRO), demonstrates MRO behavior with multiple inheritance examples, and presents advanced techniques such as metaclasses, descriptors, class decorators, __slots__, and singleton patterns.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
Understanding Python super() and Method Resolution Order (MRO) with Advanced Examples

Introduction

In Python, super() does not simply call a parent class method; it calls the method of the next class in the Method Resolution Order (MRO), which defines the order Python searches for methods in a class hierarchy.

MRO Mechanics

Python's MRO follows the C3 linearization algorithm, a depth‑first left‑to‑right strategy that guarantees each class appears only once and that the order of multiple base classes is deterministic.

Example

Assume the following class hierarchy:

class A:
    def do_something(self):
        print("A doing something")

class B(A):
    def do_something(self):
        print("B doing something")
        super().do_something()

class C(A):
    def do_something(self):
        print("C doing something")
        super().do_something()

class D(B, C):
    def do_something(self):
        print("D doing something")
        super().do_something()

Class D inherits from B and C , both of which inherit from A . When D.do_something() is called, super().do_something() follows the MRO and invokes the next class's method.

Viewing the MRO

You can inspect a class's MRO via the __mro__ attribute:

print(D.__mro__)
# Output: (
,
,
,
,
)

Calling the Next Method in the MRO

Inside D.do_something() , super().do_something() calls B.do_something() (the next class in D's MRO), not A directly.

Complete Code

class A:
    def do_something(self):
        print("A doing something")

class B(A):
    def do_something(self):
        print("B doing something")
        super().do_something()

class C(A):
    def do_something(self):
        print("C doing something")
        super().do_something()

class D(B, C):
    def do_something(self):
        print("D doing something")
        super().do_something()

d = D()
d.do_something()
# Output:
# D doing something
# B doing something
# C doing something
# A doing something

Advanced Examples

Metaclass for Dynamic Class Creation

class AutoStrMeta(type):
    def __new__(cls, name, bases, attrs):
        attrs['__str__'] = lambda self: f"{name}({', '.join(f'{k}={v}' for k, v in vars(self).items())})"
        return super().__new__(cls, name, bases, attrs)

class MyClass(metaclass=AutoStrMeta):
    def __init__(self, x, y):
        self.x = x
        self.y = y

obj = MyClass(10, 20)
print(obj)  # Output: MyClass(x=10, y=20)

Descriptor Protocol for Attribute Validation

class PositiveInteger:
    def __init__(self, initial_value=None):
        self.value = initial_value
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, value):
        if not isinstance(value, int) or value < 0:
            raise ValueError("Value must be a positive integer.")
        self.value = value

class MyClass:
    count = PositiveInteger()
    def __init__(self, count):
        self.count = count

obj = MyClass(10)
print(obj.count)  # Output: 10
obj.count = -5    # Raises ValueError

Class Decorator for Automatic Subclass Registration

registry = []

def register_class(cls):
    registry.append(cls)
    return cls

@register_class
class MyClass1:
    def method(self):
        return "MyClass1"

@register_class
class MyClass2:
    def method(self):
        return "MyClass2"

def create_instance(index):
    return registry[index]()

instance1 = create_instance(0)
instance2 = create_instance(1)
print(instance1.method())  # Output: MyClass1
print(instance2.method())  # Output: MyClass2

Using __slots__ and __getstate__/__setstate__ for Serialization Optimization

import pickle

class MyClass:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __getstate__(self):
        return {'x': self.x, 'y': self.y}
    def __setstate__(self, state):
        self.x = state['x']
        self.y = state['y']

obj = MyClass(10, 20)
serialized = pickle.dumps(obj)
deserialized = pickle.loads(serialized)
print(deserialized.x)  # Output: 10
print(deserialized.y)  # Output: 20

Singleton Pattern via __new__ and __init__

class Singleton:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance
    def __init__(self, value):
        if not hasattr(self, '_initialized'):
            self.value = value
            self._initialized = True

singleton1 = Singleton('first')
singleton2 = Singleton('second')
print(singleton1.value)  # Output: first
print(singleton2.value)  # Output: first
print(singleton1 is singleton2)  # Output: True

These examples illustrate how super() follows the MRO to ensure consistent method resolution in multiple inheritance scenarios, and they demonstrate several advanced Python techniques for dynamic class creation, attribute validation, registration, memory optimization, serialization, and singleton implementation.

PythonsingletonsuperMROMultiple Inheritanceadvanced OOPdescriptormetaclass
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.