Python Super Keyword Tutorial

Avatar

By squashlabs, Last Updated: September 24, 2023

Python Super Keyword Tutorial

Introduction to Super

In Python, the super keyword is used to refer to the superclass or parent class of a class. It provides a way to access the methods and properties of the superclass, allowing for code reuse and facilitating inheritance. The super keyword is commonly used in object-oriented programming to invoke the superclass’s methods without explicitly naming the superclass.

super() is not a function, but a special syntax that allows you to call a method from the superclass. It is typically used within the context of a subclass to call the superclass’s methods. By using super(), you can avoid hardcoding the superclass name, which makes your code more flexible and maintainable.

Here’s a basic example of using super() to call a method from the superclass:

class Parent:
    def __init__(self):
        self.name = "Parent"

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

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.name = "Child"

child = Child()
child.greet()  # Output: Hello from Child!

In this example, the Child class inherits from the Parent class. The Child class overrides the __init__() method and calls the superclass’s __init__() method using super().__init__(). This allows the Child class to initialize both its own attributes and the attributes inherited from the Parent class.

Related Article: How to Use Python Super With Init Methods

The Anatomy of Super

The super() keyword takes two arguments: the subclass that is calling the superclass’s method, and the instance of the subclass. These arguments are automatically passed to the superclass’s method, allowing the superclass to access the instance variables and methods of the subclass.

The syntax for using super() is as follows:

super(Subclass, self)

Subclass: The subclass that is calling the superclass’s method.
self: The instance of the subclass.

By convention, super() is typically called within the subclass’s method definitions, usually in the constructor (__init__()) or other instance methods. When called, super() returns a temporary object of the superclass, which allows you to call the superclass’s methods.

Best Practices with Super

When using super(), it’s important to keep a few best practices in mind:

1. Always call super().__init__() in the subclass’s __init__() method if the superclass has an initializer. This ensures that the superclass’s initialization is properly executed before the subclass’s initialization.

2. Avoid using super() outside of the subclass’s method definitions. It is specifically designed for invoking superclass methods and may not work as expected in other contexts.

3. Be aware of the method resolution order (MRO) when using multiple inheritance. The MRO determines the order in which superclasses are searched for a method. You can access the MRO of a class using the __mro__ attribute.

Use Case 1: Super in Single Inheritance

In single inheritance, a class inherits from a single superclass. This is the simplest form of inheritance, and the usage of super() is straightforward.

Here’s an example of using super() in a single inheritance scenario:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print("I am an animal.")

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

    def speak(self):
        super().speak()
        print("I am a dog.")

dog = Dog("Buddy", "Golden Retriever")
dog.speak()

In this example, the Animal class is the superclass, and the Dog class is the subclass. The Dog class calls the superclass’s __init__() method using super().__init__() to initialize the name attribute. It also overrides the speak() method and calls the superclass’s speak() method using super().speak() to reuse the behavior of the Animal class.

The output of the above code will be:

I am an animal.
I am a dog.

By calling super().speak(), the Dog class first invokes the speak() method of the Animal class, and then prints the specific message for dogs.

Related Article: How to Determine the Type of an Object in Python

Use Case 2: Super in Multiple Inheritance

In Python, multiple inheritance allows a class to inherit from multiple superclasses. This can be useful when you want to combine the behavior of multiple classes into a single subclass. However, it can also introduce complexity, especially when dealing with method resolution order (MRO) and using super().

Here’s an example of using super() in a multiple inheritance scenario:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print("I am an animal.")

class Mammal:
    def __init__(self, age):
        self.age = age

    def speak(self):
        print("I am a mammal.")

class Dog(Animal, Mammal):
    def __init__(self, name, breed, age):
        super().__init__(name)
        super(Mammal, self).__init__(age)
        self.breed = breed

    def speak(self):
        super().speak()
        print("I am a dog.")

dog = Dog("Buddy", "Golden Retriever", 5)
dog.speak()

In this example, the Animal and Mammal classes are the superclasses, and the Dog class is the subclass inheriting from both superclasses. The Dog class calls the __init__() methods of both superclasses using super() to initialize the name and age attributes. It also overrides the speak() method and calls the superclasses’ speak() methods to reuse their behavior.

The output of the above code will be:

I am an animal.
I am a dog.

By calling super().speak(), the Dog class first invokes the speak() method of the Animal class, and then the speak() method of the Mammal class. Finally, it prints the specific message for dogs.

Use Case 3: Super with Diamond Inheritance

Diamond inheritance occurs when a class inherits from two separate superclasses that have a common superclass. This can lead to ambiguity in method resolution order (MRO) and require careful use of super().

Here’s an example of using super() with diamond inheritance:

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print("I am an animal.")

class Mammal(Animal):
    def __init__(self, age):
        super().__init__("Unknown")
        self.age = age

    def speak(self):
        super().speak()
        print("I am a mammal.")

class Dog(Mammal):
    def __init__(self, name, breed, age):
        super().__init__(age)
        self.name = name
        self.breed = breed

    def speak(self):
        super().speak()
        print("I am a dog.")

dog = Dog("Buddy", "Golden Retriever", 5)
dog.speak()

In this example, the Animal class is the common superclass, and the Mammal and Dog classes are the superclasses inheriting from it. The Dog class calls the __init__() methods of both superclasses using super() to initialize the name and age attributes. It also overrides the speak() method and calls the superclasses’ speak() methods to reuse their behavior.

The output of the above code will be:

I am an animal.
I am a mammal.
I am a dog.

By calling super().speak(), the Dog class first invokes the speak() method of the Mammal class, which in turn invokes the speak() method of the Animal class. Finally, it prints the specific message for dogs.

Real World Example 1: Using Super in a Web Application

The super() keyword can be particularly useful in web application development, where you often have a base class for your views or controllers that defines common functionality. By using super(), you can easily extend and customize the behavior of these base classes.

Here’s an example of using super() in a web application:

class BaseController:
    def __init__(self):
        self.middleware = []

    def add_middleware(self, middleware):
        self.middleware.append(middleware)

    def handle_request(self, request):
        for middleware in self.middleware:
            request = middleware.process_request(request)
        return self.process_request(request)

    def process_request(self, request):
        raise NotImplementedError("Subclasses must implement process_request().")

class UserController(BaseController):
    def process_request(self, request):
        # Perform user-related processing
        return "User data"

class LoggingMiddleware:
    def process_request(self, request):
        # Perform logging
        return request

class AuthenticationMiddleware:
    def process_request(self, request):
        # Perform authentication
        return request

controller = UserController()
controller.add_middleware(LoggingMiddleware())
controller.add_middleware(AuthenticationMiddleware())
result = controller.handle_request("GET /user/123")
print(result)

In this example, the BaseController class is the base class for all controllers in the web application. It defines common functionality for handling requests, but delegates the actual request processing to its subclasses by calling self.process_request(request) using super(). This allows subclasses like UserController to customize the request processing while reusing the middleware functionality provided by the base class.

The output of the above code will depend on the implementation of the UserController and the middleware classes. It demonstrates how super() can be used to extend and customize the behavior of a base class.

Related Article: How to Use Class And Instance Variables in Python

Real World Example 2: Using Super in Data Analysis

The super() keyword can also be useful in data analysis tasks, where you often have a base class for data processing that provides common methods and functionality. By using super(), you can easily extend and specialize the data processing behavior.

Here’s an example of using super() in a data analysis scenario:

import pandas as pd

class BaseDataProcessor:
    def __init__(self, data):
        self.data = data

    def preprocess_data(self):
        # Perform common data preprocessing steps
        self.data = self.data.dropna()

class SalesDataProcessor(BaseDataProcessor):
    def preprocess_data(self):
        super().preprocess_data()
        # Perform additional data preprocessing steps specific to sales data
        self.data = self.data[self.data['quantity'] > 0]

data = pd.read_csv('sales_data.csv')
processor = SalesDataProcessor(data)
processor.preprocess_data()
print(processor.data.head())

In this example, the BaseDataProcessor class is the base class for data processing tasks. It provides a common method preprocess_data() that performs general data preprocessing steps. The SalesDataProcessor class inherits from BaseDataProcessor and overrides the preprocess_data() method to add additional data preprocessing steps specific to sales data.

The output of the above code will depend on the specific data and data preprocessing steps performed. It demonstrates how super() can be used to extend and specialize the behavior of a base class in the context of data analysis.

Performance Consideration 1: Super and Memory Usage

While using super() can greatly improve code reuse and maintainability, it’s important to consider its impact on memory usage. Each call to super() creates a temporary object representing the superclass, which consumes memory. In scenarios where memory usage is a concern, excessive use of super() can lead to increased memory consumption.

To mitigate this, avoid unnecessary calls to super() and evaluate whether the benefits of code reuse outweigh the potential increase in memory usage. Consider whether alternative approaches, such as composition or a different inheritance structure, may be more memory-efficient for your specific use case.

Performance Consideration 2: Super and Execution Time

The use of super() in method resolution can have an impact on the execution time of your code, particularly in scenarios involving multiple inheritance. The time complexity of method resolution with super() is dependent on the method resolution order (MRO) of the classes involved.

In general, the time complexity of method resolution with super() is linear in the number of classes in the inheritance hierarchy. This means that as the number of superclasses increases, the time taken to resolve the method may also increase.

To optimize performance, carefully consider the design and structure of your classes and inheritance hierarchy. Minimize the depth of inheritance and avoid unnecessary levels of indirection introduced by excessive use of super(). Profile and benchmark your code to identify potential performance bottlenecks and optimize where necessary.

Advanced Technique 1: Overriding Super

In addition to calling the superclass’s method using super(), it is also possible to override the behavior of the superclass’s method in the subclass. This can be achieved by calling the superclass’s method directly or modifying the output of the superclass’s method.

Here’s an example of overriding the superclass’s method:

class Parent:
    def greet(self):
        return "Hello from Parent!"

class Child(Parent):
    def greet(self):
        return "Hi from Child!"

child = Child()
print(child.greet())  # Output: Hi from Child!

In this example, the Child class overrides the greet() method defined in the Parent class by redefining the method in the subclass. When the greet() method is called on an instance of the Child class, the overridden method in the subclass is executed instead of the method in the superclass.

Advanced Technique 2: Super and Metaclasses

Metaclasses provide a way to customize the creation and behavior of classes. They can be used in conjunction with super() to control the initialization and method resolution of classes and their superclasses.

Here’s an example of using super() with metaclasses:

class CustomMetaclass(type):
    def __new__(cls, name, bases, attrs):
        attrs['custom_attr'] = "Custom value"
        return super().__new__(cls, name, bases, attrs)

class Parent(metaclass=CustomMetaclass):
    pass

class Child(Parent):
    pass

child = Child()
print(child.custom_attr)  # Output: Custom value

In this example, the CustomMetaclass is defined as the metaclass for the Parent class. The CustomMetaclass adds a custom attribute custom_attr to the Parent class and its subclasses. When an instance of the Child class is created, it inherits the custom_attr attribute from the Parent class due to the use of super() in the metaclass’s __new__() method.

Code Snippet 1: Basic Usage of Super

class Parent:
    def greet(self):
        print("Hello from Parent!")

class Child(Parent):
    def greet(self):
        super().greet()
        print("Hi from Child!")

child = Child()
child.greet()

Output:

Hello from Parent!
Hi from Child!

This code snippet demonstrates the basic usage of super(). The Child class inherits from the Parent class and overrides its greet() method. By calling super().greet(), the Child class invokes the greet() method of the Parent class, and then adds its own greeting.

Code Snippet 2: Super in Class Methods

class Parent:
    @classmethod
    def get_name(cls):
        return "Parent"

class Child(Parent):
    @classmethod
    def get_name(cls):
        return super().get_name() + " -> Child"

print(Child.get_name())  # Output: Parent -> Child

This code snippet demonstrates the usage of super() in class methods. The Parent class defines a class method get_name() that returns the string “Parent”. The Child class overrides the get_name() method and calls the superclass’s get_name() method using super().get_name(). It then appends ” -> Child” to the returned string. The output is “Parent -> Child”.

Code Snippet 3: Super in Static Methods

class Parent:
    @staticmethod
    def say_hello():
        return "Hello from Parent"

class Child(Parent):
    @staticmethod
    def say_hello():
        return super().say_hello() + " -> Child"

print(Child.say_hello())  # Output: Hello from Parent -> Child

This code snippet demonstrates the usage of super() in static methods. The Parent class defines a static method say_hello() that returns the string “Hello from Parent”. The Child class overrides the say_hello() method and calls the superclass’s say_hello() method using super().say_hello(). It then appends ” -> Child” to the returned string. The output is “Hello from Parent -> Child”.

Code Snippet 4: Super in Property Setters

class Parent:
    def __init__(self):
        self._name = ""

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = "Parent: " + value

class Child(Parent):
    @Parent.name.setter
    def name(self, value):
        super(Child, Child).name.__set__(self, "Child: " + value)

child = Child()
child.name = "Alice"
print(child.name)  # Output: Parent: Child: Alice

This code snippet demonstrates the usage of super() in property setters. The Parent class defines a property name with a setter that prefixes the input value with “Parent: “. The Child class overrides the name setter and calls the superclass’s name setter using super(Child, Child).name.__set__(self, "Child: " + value). This sets the name property of the Parent class and appends “Child: ” to the input value. The output is “Parent: Child: Alice”.

Code Snippet 5: Super in Property Deleters

class Parent:
    def __init__(self):
        self._name = ""

    @property
    def name(self):
        return self._name

    @name.deleter
    def name(self):
        del self._name

class Child(Parent):
    @Parent.name.deleter
    def name(self):
        super(Child, Child).name.__delete__(self)

child = Child()
child.name = "Alice"
del child.name
print(hasattr(child, 'name'))  # Output: False

This code snippet demonstrates the usage of super() in property deleters. The Parent class defines a property name with a deleter that deletes the _name attribute. The Child class overrides the name deleter and calls the superclass’s name deleter using super(Child, Child).name.__delete__(self). This deletes the _name attribute of the Parent class. The output is False, indicating that the name attribute no longer exists on the child object.

Error Handling with Super

When using super(), it’s important to handle potential errors that may arise. One common error is the AttributeError when calling a method that doesn’t exist in the superclass.

To handle such errors, you can use try-except blocks to catch and handle exceptions that may occur when calling super(). This allows you to gracefully handle errors and provide fallback behavior if necessary.

Here’s an example of error handling with super():

class Parent:
    def greet(self):
        print("Hello from Parent!")

class Child(Parent):
    def greet(self):
        try:
            super().greet()
        except AttributeError:
            print("Superclass has no greet() method.")

child = Child()
child.greet()

Output:

Hello from Parent!

In this example, the Child class calls super().greet() within a try-except block. If the Parent class doesn’t have a greet() method, an AttributeError will be raised. The except block catches the exception and prints a fallback message.

It’s important to note that proper error handling and fallback behavior depend on your specific use case and requirements.