Inheritance lets you create a new class that extends and specialises an existing class โ inheriting all its attributes and methods, and adding or overriding behaviour as needed. This is the mechanism behind SQLAlchemy’s declarative model system (all your models inherit from Base), Pydantic’s schema inheritance, and FastAPI’s exception hierarchy. Understanding inheritance, method overriding, and super() is essential for reading and writing FastAPI application code, where extending base classes is how you configure the framework’s behaviour.
Basic Inheritance
# Base class (parent)
class Animal:
def __init__(self, name: str, sound: str):
self.name = name
self.sound = sound
def speak(self) -> str:
return f"{self.name} says {self.sound}"
def describe(self) -> str:
return f"I am {self.name}"
# Child class โ inherits all of Animal's methods and attributes
class Dog(Animal):
def __init__(self, name: str):
# super() calls the parent class's __init__
super().__init__(name, sound="Woof")
self.tricks = [] # new attribute specific to Dog
def learn_trick(self, trick: str) -> None:
self.tricks.append(trick)
def show_tricks(self) -> str:
if not self.tricks:
return f"{self.name} knows no tricks"
return f"{self.name} knows: {', '.join(self.tricks)}"
# Override the parent's speak method
def speak(self) -> str:
return f"{self.name} barks: Woof woof!" # different from Animal
# Usage
rex = Dog("Rex")
print(rex.speak()) # Rex barks: Woof woof! (overridden)
print(rex.describe()) # I am Rex (inherited from Animal)
rex.learn_trick("sit")
print(rex.show_tricks()) # Rex knows: sit
# isinstance checks the full hierarchy
print(isinstance(rex, Dog)) # True
print(isinstance(rex, Animal)) # True โ Dog IS-A Animal
print(type(rex) == Animal) # False โ exact type is Dog
super() returns a proxy object that delegates method calls to the parent class. In __init__, super().__init__(...) ensures the parent class is properly initialised before you add child-specific attributes. Forgetting to call super().__init__() when overriding __init__ is one of the most common inheritance bugs โ the parent’s attributes never get set, and any method that uses them will fail with an AttributeError.Base (or DeclarativeBase in SQLAlchemy 2.0): class Post(Base): __tablename__ = "posts". FastAPI’s exception system works the same way โ you can create custom exceptions by inheriting from HTTPException. Learning inheritance here means the SQLAlchemy and FastAPI chapters will feel natural rather than magical.MyClass.__mro__. In practice, for FastAPI development, you will rarely need multiple inheritance beyond inheriting from one application base class and one mixin.super() and Method Overriding
class AppError(Exception):
"""Base class for application-specific errors."""
def __init__(self, message: str, status_code: int = 500):
super().__init__(message) # call Exception.__init__
self.message = message
self.status_code = status_code
def to_dict(self) -> dict:
return {"error": self.message, "status_code": self.status_code}
class NotFoundError(AppError):
def __init__(self, resource: str, resource_id: int):
# Build a specific message and pass 404 status to parent
super().__init__(
message=f"{resource} with id={resource_id} not found",
status_code=404
)
self.resource = resource
self.resource_id = resource_id
class ValidationError(AppError):
def __init__(self, field: str, issue: str):
super().__init__(message=f"Validation failed on '{field}': {issue}",
status_code=422)
self.field = field
# Override to_dict to add field info
def to_dict(self) -> dict:
base = super().to_dict() # get parent's dict
base["field"] = self.field # add child-specific field
return base
# Using the hierarchy
try:
raise NotFoundError("Post", 42)
except AppError as e: # catches ANY AppError subclass
print(e.message) # Post with id=42 not found
print(e.status_code) # 404
print(e.to_dict()) # {"error": "...", "status_code": 404}
err = ValidationError("email", "must be a valid email address")
print(err.to_dict())
# {"error": "Validation failed on 'email': ...", "status_code": 422, "field": "email"}
Abstract Base Classes
from abc import ABC, abstractmethod
# ABC โ Abstract Base Class โ cannot be instantiated directly
# Forces subclasses to implement abstract methods
class Repository(ABC):
"""Abstract repository โ defines the interface all repositories must implement."""
@abstractmethod
def get(self, id: int):
"""Return the item with the given ID, or None."""
pass
@abstractmethod
def create(self, data: dict):
"""Create a new item and return it."""
pass
@abstractmethod
def delete(self, id: int) -> bool:
"""Delete item by ID. Return True if deleted."""
pass
# Concrete implementation
class PostRepository(Repository):
def __init__(self):
self._store = {}
self._next_id = 1
def get(self, id: int):
return self._store.get(id)
def create(self, data: dict):
post = {**data, "id": self._next_id}
self._store[self._next_id] = post
self._next_id += 1
return post
def delete(self, id: int) -> bool:
return self._store.pop(id, None) is not None
# repo = Repository() # TypeError: Can't instantiate abstract class
repo = PostRepository() # โ
post = repo.create({"title": "Hello", "body": "World"})
print(repo.get(1)) # {"title": "Hello", "body": "World", "id": 1}
Mixins โ Reusable Behaviour
# Mixin: a class that adds behaviour without being a complete class itself
# Convention: name ends with "Mixin"
class TimestampMixin:
"""Adds created_at and updated_at to any class."""
def __init__(self, *args, **kwargs):
from datetime import datetime
super().__init__(*args, **kwargs)
self.created_at = datetime.utcnow()
self.updated_at = datetime.utcnow()
def touch(self):
from datetime import datetime
self.updated_at = datetime.utcnow()
class SoftDeleteMixin:
"""Adds soft-delete support (marks as deleted, does not remove)."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.is_deleted = False
def soft_delete(self):
self.is_deleted = True
# Combine mixins with a real class
class Post(TimestampMixin, SoftDeleteMixin):
def __init__(self, title: str, body: str):
super().__init__() # calls mixins' __init__ via MRO
self.title = title
self.body = body
p = Post("Hello", "World")
print(p.created_at) # datetime(...)
print(p.is_deleted) # False
p.soft_delete()
print(p.is_deleted) # True
Common Mistakes
Mistake 1 โ Forgetting super().__init__() in child class
โ Wrong โ parent attributes never set:
class Dog(Animal):
def __init__(self, name: str):
# forgot super().__init__()!
self.tricks = []
rex = Dog("Rex")
print(rex.name) # AttributeError โ name was never set by Animal.__init__
โ Correct โ always call super().__init__():
def __init__(self, name: str):
super().__init__(name, "Woof") # โ parent initialised first
self.tricks = []
Mistake 2 โ Using isinstance() vs type() for checks
โ Wrong โ type() fails for subclasses:
if type(error) == AppError: # False for NotFoundError even though it IS an AppError!
handle(error)
โ Correct โ isinstance() respects inheritance:
if isinstance(error, AppError): # True for AppError AND all subclasses โ
handle(error)
Mistake 3 โ Overriding a method without calling super() when needed
โ Wrong โ parent method’s work is lost:
class LoggedRepo(PostRepository):
def create(self, data: dict):
print("Creating post...") # forgot to call super().create(data)!
# data is never actually saved
โ Correct โ call super() and extend:
def create(self, data: dict):
result = super().create(data) # do the actual work โ
print(f"Created post id={result['id']}")
return result
Quick Reference
| Concept | Code |
|---|---|
| Inherit from parent | class Child(Parent): |
| Call parent init | super().__init__(...) |
| Override method | Redefine same method name in child |
| Call parent method | super().method_name(...) |
| Check type (with subclasses) | isinstance(obj, ParentClass) |
| Inspect MRO | MyClass.__mro__ |
| Abstract method | @abstractmethod on method in ABC subclass |
| Prevent instantiation | Inherit from ABC |