Inheritance, Polymorphism and Method Overriding

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
Note: 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.
Tip: SQLAlchemy uses inheritance as its core mechanism. Every database model inherits from 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.
Warning: Python supports multiple inheritance (a class can inherit from multiple parents), but it should be used carefully. The MRO (Method Resolution Order) determines which parent’s method is called when both define the same method. Python uses the C3 linearisation algorithm for MRO. You can inspect it with 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

🧠 Test Yourself

You define a DatabaseError(AppError) class that calls super().__init__(message, status_code=500). A FastAPI exception handler catches AppError exceptions. Will it also catch DatabaseError?