The zero-argument super() relies on a compiler-generated closure cell named __class__ that is implicitly created for any method defined lexically within a Python class body. When the compiler processes a class definition, it creates a cell variable __class__ in the method's closure that points to the class object currently being defined. When super() is called without arguments, the C implementation inspects the calling frame, locates this __class__ cell, and uses it as the first argument (the type). It then uses the first positional argument of the method (usually self) as the instance. This mechanism binds the class reference at definition time rather than call time, eliminating the need to hardcode class names while ensuring that each method in an inheritance chain refers to its own specific position in the MRO (Method Resolution Order).
class Base: def method(self): return "Base" class Middle(Base): def method(self): # __class__ is bound to Middle here return f"Middle -> {super().method()}" class Derived(Middle): def method(self): # __class__ is bound to Derived here return f"Derived -> {super().method()}"
We maintained a quantitative trading library with a deep hierarchy of pricing models. The BaseModel provided a calculate_risk() method, EquityModel overrode it to add stock-specific logic, and AmericanOptionModel further specialized it. During a major refactor to rename EquityModel to VanillaEquityModel, we discovered dozens of stale super(EquityModel, self) calls in mixin classes that had been copy-pasted. These stale references caused TypeError or silent logical errors where the wrong parent method was invoked, breaking production risk calculations.
Solution 1: Global search-and-replace refactoring. We considered using automated tools to find and replace all hardcoded class names in super() calls across the 200,000-line codebase. Pros: It requires no architectural changes and works with legacy Python 2 syntax. Cons: It is fragile and incomplete; it misses dynamically generated classes, string-based dynamic method assignments, and references in third-party extensions. It also violates the DRY principle, as the class name is repeated in every method.
Solution 2: Universal adoption of zero-argument super(). We migrated the entire codebase to use super() without arguments. Pros: This makes class renaming completely safe, eliminates a major source of human error during refactoring, and significantly improves readability by removing redundant noise. It correctly handles complex cooperative multiple inheritance patterns. Cons: It requires Python 3.6+ (which we had), and developers unfamiliar with the implicit closure mechanism initially found it confusing. It also cannot be used in functions dynamically attached to classes after definition.
Solution 3: Metaclass injection of class references. We briefly considered using a metaclass to inject a _defining_class attribute into every method. Pros: This makes the mechanism explicit and inspectable. Cons: It adds significant complexity and overhead, conflicts with standard CPython optimization, and reinvents a feature already provided by the language compiler.
We chose Solution 2. The migration was completed over one sprint. The result was a 40% reduction in time spent on subsequent refactoring tasks involving class renaming, and the elimination of an entire class of bugs related to stale super() references in our CI pipeline.
How does super() physically locate the __class__ cell when called with zero arguments?
The implementation of super() in CPython (in Objects/typeobject.c) uses PyEval_GetLocals() to inspect the calling frame's local variables and closure. It specifically searches for a free variable (cell) named __class__. This cell is only created by the compiler when a function is defined lexically inside a class body (indicated by the CO_OPTIMIZED flag and the class scope). If the cell is found, super() extracts the class object; if not, it raises RuntimeError: super(): __class__ cell not found. The zero-argument form is essentially transformed by the compiler into super(__class__, self), where __class__ is the closed-over variable.
What happens if you try to use zero-argument super() inside a function that is assigned to a class attribute after the class is created?
If you define a function outside of a class body and then assign it as a method (e.g., MyClass.method = some_function), calling super() inside that function will raise a RuntimeError. This occurs because the compiler only creates the __class__ cell for code objects compiled as part of a class suite. Without the cell, super() has no way to determine which class in the hierarchy is the "current" class, as it cannot distinguish between the function's definition scope and the class it was later attached to.
Why does zero-argument super() not cause infinite recursion when a subclass method calls super() and the parent method also calls super()?
This works because __class__ refers to the class where the method is defined, not the runtime class of the instance (type(self)). When Derived.method() calls super(), it finds __class__ is Derived and delegates to the next class in Derived.__mro__ (e.g., Middle). When Middle.method() is reached and it calls super(), its own distinct __class__ cell contains Middle, so it looks up the next class after Middle (e.g., Base). Each level of the hierarchy uses its own definition-time class reference, ensuring the MRO is traversed linearly upward exactly once without looping back to the subclass.