27.1 Single Inheritance: Subclassing and super()
In single inheritance, a class (known as the subclass or derived class) inherits attributes and methods from a single other class (the superclass or base class). This is the simplest and most common form of inheritance, forming a direct, linear hierarchy. The primary mechanism for leveraging the superclass’s functionality in the subclass is the super() function.
The super() Function Explained
The super() function returns a temporary “superobject” that allows you to access methods and properties from the superclass. Its primary purpose is to enable cooperative inheritance, where a subclass can extend the functionality of its superclass rather than completely replacing it. This is crucial for maintaining code reusability and avoiding the duplication of logic.
A common misconception is that super() simply “calls the parent class.” While this is often the observable outcome, its behavior is more nuanced and becomes critically important in multiple inheritance. super() follows the Method Resolution Order (MRO) to determine which class’s method to call next. In single inheritance, the MRO is straightforward, so super() reliably calls the immediate parent.
class Vehicle:
def __init__(self, make, model):
self.make = make
self.model = model
self.engine_started = False
def start_engine(self):
self.engine_started = True
return "Engine sound..."
class Car(Vehicle): # Single inheritance: Car -> Vehicle
def __init__(self, make, model, num_doors):
# Using super() to call the __init__ of Vehicle
super().__init__(make, model) # Equivalent to Vehicle.__init__(self, make, model)
self.num_doors = num_doors
def start_engine(self):
# Extend the parent's method by calling it first
base_sound = super().start_engine()
return f"{base_sound} Vroom! Vroom! ({self.make} {self.model})"
# Example usage
my_car = Car("Toyota", "Corolla", 4)
print(my_car.start_engine()) # Output: Engine sound... Vroom! Vroom! (Toyota Corolla)
print(f"Doors: {my_car.num_doors}") # Output: Doors: 4
print(f"Make and Model: {my_car.make} {my_car.model}") # Output: Make and Model: Toyota Corolla
Why Use super() Over Explicit Parent Calls?
You might wonder why you should use super().__init__() instead of the more explicit Vehicle.__init__(self, make, model). The key reason is maintainability and flexibility. If you ever change the name of the base class Vehicle or the class hierarchy itself (e.g., insert a new class between Car and Vehicle), the explicit call would need to be updated everywhere. The super() call is dynamic and automatically adapts to changes in the inheritance structure. This makes your code more robust and easier to refactor.
Common Pitfalls and Edge Cases
One of the most frequent errors in single inheritance is forgetting to call super().__init__(). If a subclass defines its own __init__ method, it overrides the superclass’s __init__. The superclass’s initializer is never automatically called. This can lead to instances of the subclass that are missing attributes that the superclass’s __init__ would have set, resulting in AttributeError exceptions later.
class Bicycle(Vehicle):
def __init__(self, frame_size):
# We forgot to call super().__init__(make, model)
self.frame_size = frame_size
my_bike = Bicycle("54cm")
# my_bike.make and my_bike.model do not exist!
# print(my_bike.make) # This would raise an AttributeError
Another subtle pitfall involves the arguments passed to super(). In Python 3, you can typically call super() without arguments, and the interpreter will automatically fill in the current class and instance. However, the full signature is super(type, object_or_type). While the zero-argument form is recommended for almost all cases, understanding the full signature is necessary for advanced metaprogramming or when working with class methods.
Best Practices for super() in Initialization
- Always Call the Superclass Initializer: Unless you have a very specific reason not to, your subclass’s
__init__method should always include a call tosuper().__init__(). - Pass Necessary Arguments: Ensure you pass all required arguments from the subclass’s
__init__to the superclass’s__init__. This often involves using*argsand**kwargsto cleanly pass arguments up the chain, especially in complex hierarchies. - Call It at the Right Time: The call to
super().__init__()can be placed anywhere in the subclass initializer. However, a common and safe practice is to call it first. This ensures the superclass is fully initialized before the subclass adds its own attributes or performs operations that might depend on the superclass’s state. There are exceptions, but this is a good default rule.
class Motorcycle(Vehicle):
def __init__(self, make, model, helmet_included):
# Best practice: initialize the superclass first.
super().__init__(make, model)
# Now it's safe to proceed with subclass-specific initialization.
self.helmet_included = helmet_included
if self.engine_started: # This attribute was set by Vehicle.__init__
print("Be careful!")