Python runtime_checkable and isinstance Protocols

Python's typing.Protocol introduces structural subtyping (static duck typing), but protocols cannot be used with runtime type checks by default. The @typing.runtime_checkable decorator bridges this gap by allowing protocols to be used with isinstance() and issubclass(). This article explains how @typing.runtime_checkable enables dynamic protocol validation, how it evaluates objects under the hood, and the key limitations developers must consider when using it.

The Default Behavior of Protocols

In standard Python static typing, a Protocol defines an interface for static type checkers like Mypy or Pyright:

from typing import Protocol

class Closable(Protocol):
    def close(self) -> None:
        ...

If you attempt to validate an object against this protocol dynamically using isinstance(obj, Closable), Python raises a runtime error:

TypeError: Instance and class checks can only be used with @runtime_checkable protocols

By default, protocols exist purely for static analysis and discard their structural definitions during runtime evaluations to minimize performance overhead.

The Function of @typing.runtime_checkable

Marking a protocol with @typing.runtime_checkable alters its metaclass implementation, enabling Python's dynamic type-checking functions (isinstance() and issubclass()) to inspect the target object at runtime:

from typing import Protocol, runtime_checkable

@runtime_checkable
class Closable(Protocol):
    def close(self) -> None:
        ...

class Resource:
    def close(self) -> None:
        print("Resource closed")

res = Resource()
print(isinstance(res, Closable))  # Returns: True

When @runtime_checkable is applied, isinstance(obj, Closable) checks whether the object obj possesses all the attributes and methods defined inside the Closable protocol. Explicit inheritance from the protocol is not required; any class that implements the required members will evaluate to True.

How the Runtime Validation Works

When isinstance(obj, Protocol) executes, Python inspects the object's attribute dictionary:

  1. Attribute Existence: It verifies that every non-callable variable defined in the protocol exists on the instance or class.
  2. Callable Verification: It ensures that every method declared in the protocol exists on the object and is callable.

Critical Limitations

While @typing.runtime_checkable allows structural checks, it performs only a shallow verification:

Because of these limitations, @typing.runtime_checkable provides basic runtime structural duck-typing rather than complete type safety. Full type contracts should still be enforced with static analysis tools.