Protocol vs ABC — Structural vs Nominal Typing
Picture a regular Protocol as a purely theoretical blueprint for type checkers to compare against at design time, like an idea in your head. Adding @runtime_checkable is like physically building a transparent model of that blueprint, allowing you to actually inspect objects against it at runtime.
@runtime_checkable if you need to perform dynamic isinstance() or issubclass() checks on them at runtime.The Setup
You are writing a plugin system. You want to support dynamic third-party loggers that provide a .log() method, using a Protocol. At runtime, you need to assert that a configured plugin conforms to this interface before adding it to your system registry.
What Does This Print?
from typing import Protocol
class Logger(Protocol):
def log(self, message: str) -> None: ...
class CustomPlugin:
def log(self, message: str) -> None:
print(f"[Plugin]: {message}")
plugin = CustomPlugin()
# Verify that our plugin matches the Protocol definition
print(isinstance(plugin, Logger))
The Output
By default, typing.Protocol is purely a static analyzer tool used during linting/compilation. Unlike standard classes or Abstract Base Classes (ABCs), standard Protocols do not store runtime interfaces for isinstance() checks. Trying to evaluate isinstance() against a Protocol raises a runtime TypeError.
Why Python Does This
Under the hood, Protocol behaves as a structural type check system (duck typing). ABCs, on the other hand, enforce nominal typing—the class must explicitly subclass the ABC to pass isinstance(). Performing a structural check at runtime requires Python to parse class attributes and methods on the fly, which is computationally expensive. Python protects runtime performance by disallowing isinstance on Protocols unless they are decorated with @typing.runtime_checkable, which explicitly implements a dynamic attribute checking routine inside the protocol's metaclass __instancecheck__ method.
The Fix
from typing import Protocol, runtime_checkable
# Adding runtime_checkable compiles helper methods allowing isinstance analysis
@runtime_checkable
class Logger(Protocol):
def log(self, message: str) -> None: ...
class CustomPlugin:
def log(self, message: str) -> None:
print(f"[Plugin]: {message}")
plugin = CustomPlugin()
# This now correctly returns True because the structural match succeeds
print(isinstance(plugin, Logger))
Decorating a Protocol with @runtime_checkable explicitly tells the Python interpreter to generate the necessary runtime machinery. This allows isinstance() and issubclass() to perform structural checks by inspecting the object's methods and attributes against the protocol's definition.
How This Fails in Real Systems
An API Gateway microservice relied on third-party adapters for storage. A developer added a StorageProtocol to support multiple backends but used standard isinstance checks at the initialization boundary. On production deployment, the container cluster entered a crash loop immediately, resulting in 15 minutes of downtime due to unhandled TypeError exceptions during startup configuration parsing.
Key Takeaway
@runtime_checkable if you need to perform dynamic isinstance() or issubclass() checks on them at runtime.Protocols behave like ABCs or regular classes at runtime, expecting isinstance() to work out of the box for structural type checking.