Python FAQ: Top Questions
26. Explain the difference between `super()` and direct base class calling. When should you use `super()`?
In Python, when a class inherits from one or more parent classes, there are two primary ways to call methods of those parent classes from within the subclass: directly calling the base class method, or using `super()`. While both can achieve similar results in simple single inheritance, `super()` offers crucial advantages, especially in complex inheritance scenarios.
1. Direct Base Class Calling:
- Syntax: `BaseClassName.method_name(self, args...)`
- Mechanism: You explicitly refer to the base class by its name. This call is a static, direct call to that specific class's method. You must pass `self` (the instance) as the first argument manually, as you're calling it like a regular function belonging to `BaseClassName`.
-
Pros:
- Simple and explicit in very basic single inheritance.
- Can be useful if you *specifically* want to bypass the MRO and call a method from a particular ancestor, even if it's not the next one in the MRO (though this is rare and often a sign of design issues).
-
Cons (Why it's generally discouraged):
- Breaks Multiple Inheritance: Does not work correctly with multiple inheritance or complex class hierarchies, especially when dealing with the "diamond problem." If a common ancestor's `__init__` (or other method) is called directly by multiple subclasses, it can be called multiple times, leading to unexpected behavior or errors.
- Lack of Polymorphism: It hardcodes the base class name. If you change the inheritance hierarchy later (e.g., insert a new class between the current class and its direct base), you'd have to manually update all direct calls.
- Does not interact with MRO: It bypasses Python's Method Resolution Order (MRO), which is crucial for cooperative inheritance.
2. `super()`:
-
Syntax:
`super().method_name(args...)` (in Python 3, `super().__init__(args...)` for constructor)
Older Python 2 syntax (less common now): `super(CurrentClass, self).method_name(args...)` - Mechanism: `super()` returns a proxy object that delegates method calls to the next class in the Method Resolution Order (MRO) of the current class. It automatically handles passing the instance (`self`) and the current class in Python 3. It's designed for **cooperative inheritance**.
-
Pros (Why it's preferred):
- Handles Multiple Inheritance Correctly: The primary reason to use `super()`. It ensures that methods (especially `__init__`) in a complex hierarchy are called exactly once and in the correct MRO order, preventing the "diamond problem."
- Adheres to MRO: It respects the class's Method Resolution Order, making method calls predictable and robust in complex inheritance.
- Flexible and Maintainable: No hardcoding of parent class names. If the inheritance hierarchy changes, `super()` automatically adapts, making your code more resilient to refactoring.
- Clearer Intent: Explicitly states "call the next method in the hierarchy," rather than "call this specific class's method."
- Automatic Argument Passing: In Python 3, `super()` without arguments automatically knows the current class and instance, simplifying calls.
-
Cons:
- Can be slightly less intuitive for beginners when first introduced to multiple inheritance.
When to use `super()`:
You should **almost always use `super()`** when calling a method of a parent class, especially for `__init__` methods, unless you have a very specific, well-understood reason not to (which is rare). It's essential for cooperative multiple inheritance and makes your code more robust and maintainable.
Specifically, use `super()` when:
- You are defining `__init__` in a subclass and want to ensure the `__init__` methods of all parent classes in the MRO are called correctly.
- You are overriding a method in a subclass but also want to extend or call the original functionality of the parent's method.
- Your class is part of a multiple inheritance hierarchy, or you anticipate it might become part of one in the future.
- You want your code to be robust against changes in the class hierarchy (e.g., adding an intermediate class).
# --- Example 1: Single Inheritance - super() vs. Direct Call ---
print("--- Single Inheritance ---")
class Parent:
def __init__(self, value):
self.value = value
print(f"Parent __init__ called with value: {self.value}")
def greet(self):
print("Hello from Parent!")
class Child(Parent):
def __init__(self, value, name):
# 1. Using super() (Preferred)
super().__init__(value)
self.name = name
print(f"Child __init__ called with name: {self.name}")
def greet(self):
print("Hello from Child!")
# 1. Using super() to call parent's greet
super().greet()
# 2. Direct base class call (works here, but generally discouraged)
# Parent.greet(self) # This would also call Parent's greet
# Using super()
c1 = Child(10, "Alice")
c1.greet()
# --- Example 2: Multiple Inheritance and the Diamond Problem ---
print("\n--- Multiple Inheritance (Diamond Problem) ---")
class Grandparent:
def __init__(self, param):
print(f"Grandparent __init__ called with {param}")
self.param = param
class Father(Grandparent):
def __init__(self, param_f):
# Using super() ensures Grandparent.__init__ is called ONLY once by D
super().__init__(param_f)
print(f"Father __init__ called with {param_f}")
self.param_f = param_f
def specific_method(self):
print("Method from Father")
class Mother(Grandparent):
def __init__(self, param_m):
# Using super() ensures Grandparent.__init__ is called ONLY once by D
super().__init__(param_m)
print(f"Mother __init__ called with {param_m}")
self.param_m = param_m
def specific_method(self):
print("Method from Mother")
class ChildOfBoth(Father, Mother): # Order matters for MRO: Father then Mother
def __init__(self, param_c):
print(f"ChildOfBoth __init__ called with {param_c}")
# Calling super() correctly handles the MRO for the diamond problem.
# It calls Father.__init__, which calls Grandparent.__init__,
# then super() from Father.__init__ would implicitly look for next in MRO (Mother),
# but since Father already called Grandparent, Mother won't call it again.
super().__init__(param_c)
self.param_c = param_c
def display_params(self):
print(f"Params: Grandparent={self.param}, Father={self.param_f}, Mother={self.param_m}, Child={self.param_c}")
def combined_method(self):
# Calls the specific_method based on MRO (Father's first)
super().specific_method()
# To call Mother's specific_method, you might need direct access or
# adjust design, but super() follows MRO.
Mother.specific_method(self) # Direct call if you explicitly want Mother's.
print("\nMRO for ChildOfBoth:", ChildOfBoth.__mro__)
# Expected MRO: ChildOfBoth, Father, Mother, Grandparent, object
print("\n--- Initializing ChildOfBoth using super() ---")
# Only Grandparent.__init__ is called once
my_child = ChildOfBoth(100)
my_child.display_params()
my_child.combined_method()
# What if we used direct calls in Father/Mother?
print("\n--- Initializing with Hypothetical Direct Calls (Bad Practice) ---")
class BadFather(Grandparent):
def __init__(self, param_f):
Grandparent.__init__(self, param_f) # Direct call - would lead to double call in diamond
print(f"BadFather __init__ called with {param_f}")
self.param_f = param_f
class BadMother(Grandparent):
def __init__(self, param_m):
Grandparent.__init__(self, param_m) # Direct call - would lead to double call in diamond
print(f"BadMother __init__ called with {param_m}")
self.param_m = param_m
# class BadChildOfBoth(BadFather, BadMother):
# def __init__(self, param_c):
# print(f"BadChildOfBoth __init__ called with {param_c}")
# # This would cause Grandparent.__init__ to be called twice (once by BadFather, once by BadMother)
# super().__init__(param_c) # super() here would still try to resolve correctly,
# # but the problem is in BadFather/BadMother's init
# self.param_c = param_c
# If BadFather and BadMother used direct calls to Grandparent.__init__,
# and ChildOfBoth used super() calling BadFather and BadMother,
# Grandparent.__init__ would be called multiple times.
# The proper use of super() throughout the hierarchy prevents this.
Explanation of the Example Code:
-
**Single Inheritance:**
- `Child` inherits from `Parent`. In `Child.__init__`, `super().__init__(value)` correctly calls `Parent.__init__`, passing `value`.
- In `Child.greet()`, `super().greet()` calls the `greet` method of the `Parent` class (the next in `Child`'s MRO).
- The commented-out `Parent.greet(self)` demonstrates the direct call. While it works for single inheritance, it hardcodes `Parent` and doesn't leverage the MRO.
-
**Multiple Inheritance (Diamond Problem):**
- This is where `super()` shines. We have `Grandparent`, and `Father` and `Mother` both inherit from `Grandparent`. `ChildOfBoth` then inherits from both `Father` and `Mother`, creating a "diamond" shape in the inheritance hierarchy.
- When `my_child = ChildOfBoth(100)` is created, observe the output: `Grandparent __init__` is called **only once**.
- This is because `super().__init__(param_c)` in `ChildOfBoth` first calls `Father.__init__`. Inside `Father.__init__`, `super().__init__(param_f)` calls `Grandparent.__init__`. After `Father.__init__` completes, `super()` from `ChildOfBoth` (which is still processing its own `__init__`) then looks for the next class in `ChildOfBoth`'s MRO, which is `Mother`. When `Mother.__init__` is called by `super()`, it also calls `super().__init__(param_m)`. However, the MRO has already noted that `Grandparent.__init__` has been traversed. Thus, `Grandparent.__init__` is not called again by `Mother.__init__`, even though `Mother` directly inherits from `Grandparent`. This cooperative calling ensures each base class's `__init__` is called only once.
- The `combined_method` shows how `super().specific_method()` resolves to `Father.specific_method()` because `Father` is first in `ChildOfBoth`'s MRO. If you explicitly need `Mother`'s method, you might resort to a direct call like `Mother.specific_method(self)`, but this should be carefully considered as it bypasses MRO for that specific call.
- The commented-out `BadFather`/`BadMother` example illustrates the problem if direct calls (`Grandparent.__init__(self, param_f)`) were used instead of `super()`. In such a scenario, if `ChildOfBoth` inherited from `BadFather` and `BadMother`, `Grandparent.__init__` would be called twice (once by `BadFather`, once by `BadMother`), which is usually undesirable.
The examples clearly show that `super()` is the robust and recommended way to call parent methods, especially constructors, as it seamlessly integrates with Python's MRO to handle complex inheritance patterns correctly and cooperatively, ensuring methods are called once and in the correct order.