Swiftorial Logo
Home
Swift Lessons
Tutorials
Learn More
Career
Resources

Python FAQ: Top Questions

47. Explain Python's `super()` vs direct parent class call.

This question is a deeper dive into the nuances of `super()` that were touched upon in Q45 (What is Python's `super()` function and how does it work?). While `super()` was explained generally there, here we'll explicitly contrast it with directly calling a parent class's method, highlighting why `super()` is almost always the preferred approach, especially in multiple inheritance.

Direct Parent Class Call:

A direct parent class call explicitly invokes a method from a specific parent class using the class name and passing the instance manually. The syntax is typically `ParentClass.method_name(self, *args, **kwargs)`.

  • **Syntax:** `ParentClass.method_name(self, arguments)`
  • **Behavior:** Explicitly calls the method of `ParentClass` (or whatever class you specify). It does not consult the MRO to find the "next" method.
  • **Implication:**
    • **Hardcoded Dependency:** Creates a tight coupling between the child class and a specific parent class. If the class hierarchy changes (e.g., an intermediate class is inserted), you might need to manually update all direct calls.
    • **Breaks Cooperative Inheritance:** In multiple inheritance, if the same method is implemented by multiple parents, directly calling one might prevent others in the MRO from being called, leading to incomplete or incorrect behavior (e.g., the "diamond problem" where a common ancestor's `__init__` might be called multiple times or not at all).
    • **Limited Polymorphism:** Less flexible in polymorphic scenarios where methods need to adapt to the actual type of the calling object within a complex hierarchy.

`super()` Function:

`super()` (especially the no-argument form `super().method_name()`) is designed for cooperative multiple inheritance and dynamic MRO traversal. It calls the method of the *next class in the MRO*.

  • **Syntax:** `super().method_name(arguments)` (Python 3, no arguments) or `super(CurrentClass, self).method_name(arguments)`
  • **Behavior:** Dynamically determines the next method to call based on the class's Method Resolution Order (MRO).
  • **Implication:**
    • **Flexible and Maintainable:** Adapts automatically to changes in the inheritance hierarchy because it doesn't hardcode parent names.
    • **Enables Cooperative Inheritance:** In multiple inheritance, it ensures that all relevant methods in the MRO are called exactly once in the correct order, resolving issues like the "diamond problem" cleanly. Each class contributes its piece of the logic and then delegates to the next.
    • **Robust for Complex Hierarchies:** Makes it easier to build complex, extensible class designs.

Why `super()` is Preferred:

Aspect Direct Parent Call (`ParentClass.method(self, ...)`) `super().method(...)`
Hardcoding Parent Yes, explicitly names a parent. No, dynamically finds the next in MRO.
Inheritance Changes Fragile; might require manual updates if MRO changes. Robust; automatically adapts to MRO changes.
Multiple Inheritance Problematic; can lead to duplicate calls or missed calls (diamond problem). Enables cooperative inheritance; calls methods exactly once in MRO order.
Cooperation Non-cooperative; focuses on a specific parent's method. Cooperative; allows each class in MRO to contribute.
Readability Can be clear in simple cases. Clearer for complex inheritance, aligns with Pythonic practices.
Primary Use Very rare, perhaps for specific mixin patterns where you explicitly want to skip MRO. Standard for calling parent `__init__`, and for all method calls in complex inheritance.

In almost all cases, particularly when working with inheritance (even single inheritance) or if there's any chance of evolving into multiple inheritance, you should use `super()` to call parent methods. It's more robust, flexible, and adheres to Python's design philosophy for object-oriented programming.


# --- Example 1: Single Inheritance - super() vs Direct Call ---
print("--- Single Inheritance: super() vs Direct Call ---")

class Base:
    def __init__(self, name):
        self.name = name
        print(f"Base __init__ for {self.name}")

    def greet(self):
        print(f"Hello from Base, {self.name}")

class SubWithSuper(Base):
    def __init__(self, name, id):
        super().__init__(name) # Recommended: uses super()
        self.id = id
        print(f"SubWithSuper __init__ for {self.name}, ID {self.id}")

    def greet(self):
        print(f"Hello from SubWithSuper, {self.name}")
        super().greet() # Recommended: uses super()

class SubWithDirect(Base):
    def __init__(self, name, id):
        Base.__init__(self, name) # NOT recommended: direct call
        self.id = id
        print(f"SubWithDirect __init__ for {self.name}, ID {self.id}")

    def greet(self):
        print(f"Hello from SubWithDirect, {self.name}")
        Base.greet(self) # NOT recommended: direct call

print("Creating SubWithSuper instance:")
s_super = SubWithSuper("Alice", 1)
s_super.greet()

print("\nCreating SubWithDirect instance:")
s_direct = SubWithDirect("Bob", 2)
s_direct.greet()

# Observation: For single inheritance, direct call *seems* to work the same.
# However, this hides issues if MRO changes later.


# --- Example 2: Multiple Inheritance - Diamond Problem with super() ---
print("\n--- Multiple Inheritance (Diamond Problem) with super() ---")

class Person:
    def __init__(self, name):
        self.name = name
        print(f"Person __init__ for {self.name}")

    def info(self):
        print(f"Person: {self.name}")

class Employee(Person):
    def __init__(self, name, emp_id):
        super().__init__(name)
        self.emp_id = emp_id
        print(f"Employee __init__ for {self.name}, ID {self.emp_id}")

    def info(self):
        super().info() # Delegates to Person.info()
        print(f"Employee ID: {self.emp_id}")

class Student(Person):
    def __init__(self, name, student_id):
        super().__init__(name)
        self.student_id = student_id
        print(f"Student __init__ for {self.name}, Student ID {self.student_id}")

    def info(self):
        super().info() # Delegates to Person.info()
        print(f"Student ID: {self.student_id}")

class StudentEmployee(Employee, Student): # Employee comes first in MRO
    def __init__(self, name, emp_id, student_id):
        super().__init__(name, emp_id) # This calls Employee's __init__
                                      # Employee's __init__ then calls super().__init__(name)
                                      # which (due to MRO) calls Student's __init__
                                      # Student's __init__ then calls super().__init__(name)
                                      # which (due to MRO) calls Person's __init__
        self.student_id = student_id # Re-assigning student_id since Student.__init__ didn't have emp_id.
                                      # More robust: call both parents explicitly but carefully or use mixins
                                      # for data and super for method chaining.
                                      # OR simply ensure all parents consume all args and pass them through.
        print(f"StudentEmployee __init__ for {self.name}")

    def info(self):
        super().info() # Calls Employee's info().
                       # Employee's info() calls super().info() which due to MRO calls Student's info()
                       # Student's info() calls super().info() which due to MRO calls Person's info()
        print(f"Combined Info: {self.name}")

print(f"\nMRO for StudentEmployee: {StudentEmployee.__mro__}")
# Expected MRO: (StudentEmployee, Employee, Student, Person, object)

se = StudentEmployee("Charlie", "E001", "S101")
print("\nCalling info() on StudentEmployee instance:")
se.info()

# Observe: All __init__ methods are called once. All info methods are called once,
# in the correct order determined by MRO.


# --- Example 3: Multiple Inheritance - Diamond Problem with Direct Calls (Problematic) ---
print("\n--- Multiple Inheritance (Diamond Problem) with Direct Calls (PROBLEM) ---")

class PersonD:
    def __init__(self, name):
        self.name = name
        print(f"PersonD __init__ for {self.name}")

class EmployeeD(PersonD):
    def __init__(self, name, emp_id):
        PersonD.__init__(self, name) # Direct call
        self.emp_id = emp_id
        print(f"EmployeeD __init__ for {self.name}, ID {self.emp_id}")

class StudentD(PersonD):
    def __init__(self, name, student_id):
        PersonD.__init__(self, name) # Direct call
        self.student_id = student_id
        print(f"StudentD __init__ for {self.name}, Student ID {self.student_id}")

class StudentEmployeeD(EmployeeD, StudentD):
    def __init__(self, name, emp_id, student_id):
        EmployeeD.__init__(self, name, emp_id) # First direct call
        StudentD.__init__(self, name, student_id) # Second direct call
        print(f"StudentEmployeeD __init__ for {self.name}")

# This will call PersonD.__init__ TWICE!
print("\nCreating StudentEmployeeD instance (watch for duplicate PersonD init):")
se_direct = StudentEmployeeD("Diana", "E002", "S102")

# Problem: PersonD's __init__ is called twice. This leads to inefficient initialization
# and potential incorrect state if the __init__ has side effects.
        

Explanation of the Example Code:

  • **Single Inheritance - `super()` vs Direct Call:**
    • `SubWithSuper` uses `super().__init__(name)` and `super().greet()`. This is the Pythonic way.
    • `SubWithDirect` uses `Base.__init__(self, name)` and `Base.greet(self)`. While this *appears* to work in simple single inheritance, it's brittle. If a new class were inserted between `SubWithDirect` and `Base`, `SubWithDirect` would still try to call `Base` directly, potentially skipping the new intermediate class's `__init__` or methods.
  • **Multiple Inheritance (Diamond Problem) with `super()`:**
    • This demonstrates the elegance of `super()` in complex scenarios. `StudentEmployee` inherits from `Employee` and `Student`, both of which inherit from `Person`.
    • Each `__init__` method in the hierarchy consistently uses `super().__init__()`.
    • When `StudentEmployee.__init__` is called, `super().__init__(name, emp_id)` calls `Employee.__init__`. Inside `Employee.__init__`, `super().__init__(name)` is called. Due to the MRO (`StudentEmployee`, `Employee`, `Student`, `Person`, `object`), this `super()` call from within `Employee` actually resolves to `Student.__init__`. `Student.__init__` then calls `super().__init__(name)`, which finally resolves to `Person.__init__`.
    • This ensures that `Person.__init__`, `Employee.__init__`, and `Student.__init__` are all called exactly once, in the correct MRO order, and all necessary initializations occur.
    • The `info()` method chain also demonstrates this cooperative behavior, with each `info()` calling its `super().info()` to ensure all parent `info()` methods are executed in MRO order.
  • **Multiple Inheritance (Diamond Problem) with Direct Calls (PROBLEM):**
    • In `StudentEmployeeD`, `EmployeeD.__init__` and `StudentD.__init__` both directly call `PersonD.__init__(self, name)`.
    • When `StudentEmployeeD.__init__` then directly calls both `EmployeeD.__init__(self, ...)` and `StudentD.__init__(self, ...)`, it results in `PersonD.__init__` being called **twice**. This is often an undesirable side effect, leading to redundant work or incorrect state.
    • This clearly illustrates why direct parent calls are problematic and `super()` is the preferred, robust solution for cooperative multiple inheritance.

The examples make it clear that while direct parent calls might seem to work in simple cases, `super()` is the correct and robust way to manage method calls in inheritance, especially when dealing with complex or evolving class hierarchies and multiple inheritance.