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.Protocolfor Interface Boundaries: Define Protocols at consumption boundaries (e.g. in your domain layer requiring aLoggerorRepository). This prevents domain code from depending on concrete third-party vendor packages. - Use
abc.ABCfor Shared Code Reuse: Use ABCs when parent classes provide shared concrete helper methods alongside abstract contracts.