Single inheritance and super()
Overview
Inheritance allows a new class to inherit attributes and methods from an existing class, establishing an `is-a` relationship. The `super()` function returns a proxy object that delegates method calls to a parent or sibling class. Think of inheritance like a master template and `super()` as a phone line explicitly calling the master template to avoid repeating setup logic. When overriding `__init__`, calling `super().__init__()` ensures the parent's initialization runs. class BaseWorker: def __init__(self, name): self.name = name class CeleryWorker(BaseWorker): def __init__(self, name, queue): super().__init__(name) self.queue = queue ⚠️ **Trap: Signature Mismatch** If your subclass `__init__` takes different arguments than the parent, you must explicitly pass only what the parent expects to `super().__init__()`. Passing all arguments blindly raises a `TypeError`. class User: def __init__(self, username): pass class Admin(User): def __init__(self, username, role): super().__init__(username, role) # TypeError: __init__() takes 2 positional arguments but 3 were given ⚠️ **Trap: Multiple Inheritance & MRO** Using `super()` in complex hierarchies does not just call the direct parent, it follows the Method Resolution Order (MRO). This can cause unexpected arguments to be passed to siblings. This is covered fully in `mro`.
It reduces code duplication by centralizing shared logic in base classes, making it easier to maintain and extend functionality.
Where used: Django models (`models.Model`), Pydantic `BaseModel`s, FastAPI API routers
Why learn this
- Understanding how third-party framework classes (like Django `View`s) work under the hood
- Extending existing exceptions by subclassing `Exception`
- Avoiding duplicated boilerplate across related data structures
Code walkthrough
class Worker:
def start(self):
print('worker started')
class CeleryWorker(Worker):
def start(self):
super().start()
print('connected to broker')
w = CeleryWorker()
w.start()
Focus: super().start()
Aha moment
class APIError(Exception):
def __init__(self, code):
self.code = code
class NotFoundError(APIError):
def __init__(self, code):
pass
err = NotFoundError(404)
print(hasattr(err, 'code'))
Prediction: What does this print: `True` or `False`?
Common guess: `True`
Because `NotFoundError` overrides `__init__` and does NOT call `super().__init__(code)`, the `APIError`'s `__init__` is skipped, so `self.code` is never set.
Common mistakes
- Forgetting to call `super()` in `__init__`: The parent's attributes are never initialized, leading to an `AttributeError` when those attributes are accessed.
- Redefining parent behavior instead of extending it: Completely overwriting a parent method without calling `super()` drops the parent's original functionality when you only meant to add to it.
- Passing subclass arguments directly to `super()`: When calling `super().__init__()`, you must only pass the arguments that the parent class expects. Blindly passing all of the subclass's parameters will raise a `TypeError`.
Glossary
- proxy object
- A special object that stands in for another object, forwarding or delegating commands to it. Example: `super()` returns a proxy object so that `super().__init__()` calls the parent class's `__init__` without naming the parent directly.
- base classes
- The original or parent classes that other classes are built upon and inherit features from. Example: in `class Admin(User):`, `User` is the base class.
Recall questions
- What is the primary purpose of the `super()` function in Python?
- What happens if you override the `__init__` method in a child class but do not call `super().__init__()`?
- What kind of relationship does inheritance establish between a parent and child class?
Understanding checks
What happens when a child class overrides `__init__` but forgets to call `super().__init__()`?
The parent's initialization logic will be skipped.
This is dangerous because any attributes that the parent normally sets up in its `__init__` won't be created, leading to an `AttributeError` when other inherited methods try to access them.
What does this code print?
It prints `'A'` followed by `'B'`.
The call to `b.ping()` runs the `ping` method in `B`. Inside `B.ping`, `super().ping()` delegates to the parent class `A`, printing `'A'`. Then it resumes in `B` and prints `'B'`.
What does this code do?
It raises a `TypeError`.
The subclass `UserModel` passes both `table` and `db_url` to `super().__init__()`, but the parent class `Model` only expects the `table` argument.
Practice tasks
Extend an API Request class
Modify the `SecureRequest` class so it takes a `url` and a `token`. Call `super().__init__(url)` to handle the `url`, then store the `token` as an instance variable.
Challenge
Implement a Database Model hierarchy
Create a base class `Model` with a `save` method printing `'saving to db'`. Create a `UserModel` inheriting from `Model` whose `save` prints `'validating user'` then calls the parent's `save` using `super()`.
Continue learning
Previous: Class Creation and Instantiation
Previous: Instance, Class, and Static Methods
Next: Multiple Inheritance & The Diamond Problem
Next: MRO: Method Resolution Order (C3 linearization)
Next: Method Overriding