Abstract Base Classes (ABCs) & Structural Typing (Protocols)

Designing flexible software abstractions in Python relies on two distinct type checking paradigms: Nominal Subtyping via Abstract Base Classes (abc.ABC) and Structural Subtyping via Protocols (typing.Protocol / PEP 544).

This chapter details Nominal vs Structural typing, abc.ABCMeta virtual subclass registration (@abstractmethod), static Duck Typing with typing.Protocol, and @runtime_checkable mechanics.


1. Nominal Subtyping (abc.ABC) vs. Structural Subtyping (Protocol)

  • Nominal Subtyping (abc.ABC): Subclasses must explicitly inherit from the Abstract Base Class (class PostgresRepository(Repository):). Type compatibility is determined by explicit class hierarchy names.
  • Structural Subtyping (typing.Protocol): Type compatibility is determined by shape and attributes (“Duck Typing”). Any class that implements the required methods and attributes automatically satisfies the Protocol without explicit inheritance!
Nominal vs Structural Subtyping Paradigms:

Nominal Subtyping (ABCs):
[ ABC: Repository ]  <=== Must explicitly inherit! ===  [ PostgresRepository ]

Structural Subtyping (Protocols):
[ Protocol: Renderable ]                                [ CustomWidget ]
 (Requires: draw())   <... Automatically matches! ...>  (Has: draw() method)

2. Abstract Base Classes (abc.ABC & @abstractmethod)

abc.ABC prevents instantiating incomplete base classes that lack concrete implementations of abstract methods:

from abc import ABC, abstractmethod

class BaseStorage(ABC):
    @abstractmethod
    def save(self, key: str, value: bytes) -> bool:
        """Abstract method must be implemented by concrete subclasses."""

    @abstractmethod
    def fetch(self, key: str) -> bytes:
        """Abstract fetch method."""

Instantiation Enforcement:

Attempting to instantiate BaseStorage() directly, or a subclass that fails to override all @abstractmethod definitions, raises a runtime TypeError: Can't instantiate abstract class BaseStorage with abstract methods....


3. Structural Subtyping with typing.Protocol (PEP 544)

Protocol enables Static Duck Typing. Static type checkers (Mypy, Pyright) verify that an object matches a Protocol’s shape without requiring runtime inheritance coupling:

from typing import Protocol, runtime_checkable

@runtime_checkable
class Serializer(Protocol):
    def serialize() -> str:
        ...

# Concrete class: NO inheritance from Serializer!
class UserDTO:
    def serialize(self) -> str:
        return '{"user": "alice"}'

# Mypy passes! isinstance(UserDTO(), Serializer) passes at runtime!

@runtime_checkable:

Decorating a Protocol with @runtime_checkable allows using isinstance(obj, Serializer) at runtime by inspecting whether obj contains the required protocol methods.


4. Production Architectural Guidelines

  • Use typing.Protocol for Interface Boundaries: Define Protocols at consumption boundaries (e.g. in your domain layer requiring a Logger or Repository). This prevents domain code from depending on concrete third-party vendor packages.
  • Use abc.ABC for Shared Code Reuse: Use ABCs when parent classes provide shared concrete helper methods alongside abstract contracts.
Display Options
Appearance
Text Size
100%