skip to content

What does @typing.runtime_checkable change about a typing.Protocol subclass?

level: juniorimportance: should knowfreq 42%

answer

  1. A decorator that unlocks a runtime check
  2. Without it, the check refuses outright
  3. isinstance against a protocol needs opt-in
  4. typing.runtime_checkable above class P(Protocol)
  5. Member names only, never signatures

basics

~20 s

It lets isinstance() accept that protocol as its second argument; an undecorated typing.Protocol raises TypeError instead. The decorator adds a runtime check for the protocol's member names only, and changes nothing for a static type checker.

solid answer

~40 s

`typing.Protocol` is normally a static-only construct: `isinstance(obj, P)` on an undecorated protocol raises `TypeError`, saying instance and class checks can only be used with `@runtime_checkable` protocols. Applying `@typing.runtime_checkable` opts the class into the runtime check. After that, `isinstance(obj, P)` returns True when the object has an attribute for **every member name** the protocol declares — nothing more. `issubclass(C, P)` also becomes legal, but only when the protocol declares methods and no data members. The decorator is a no-op for a static type checker, which was already verifying structural conformance including signatures. It can only be applied to a class inheriting from `typing.Protocol`; applied to anything else it raises `TypeError` at decoration time. Available since Python 3.8.

code

python · 22 lines
python
from typing import Protocol, runtime_checkable


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


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


class Handle:
    def close(self) -> None: ...


try:
    isinstance(Handle(), Closable)
except TypeError as exc:
    print("TypeError:", exc)

print(isinstance(Handle(), ClosableRT))

go deeper

for a junior

Remember the concrete failure: isinstance() against a plain typing.Protocol raises TypeError, and the fix is the @runtime_checkable decorator on the protocol class. Also recall that the runtime check only looks for member names.

for a middle

Be ready to explain the mechanics — that the decorator opts the class into the abc-based instance check, that the check is presence-only, and that issubclass() is additionally restricted to method-only protocols.

for a senior

An interviewer expects you to name the 3.12 switch from hasattr() to inspect.getattr_static() and what it broke: dynamic proxies stopped passing, and properties are no longer evaluated by a check. Say when a runtime check is worth its weakness.

for a principal

Own the policy question: where in a codebase runtime structural checks are allowed at all, versus relying on the static checker in CI plus explicit protocol subclassing at the seams. A True from this check is a hint, not a contract.

### The problem the decorator solves `typing.Protocol` (Python 3.8) describes a *shape*: a class listing the members an object must have, so a static type checker will accept any object that has them, with no inheritance relationship required. That is a compile-time idea. At runtime, `isinstance()` normally answers a *nominal* question — is this object's class somewhere in that class's inheritance tree — and a protocol has no such tree to consult. Rather than silently return a misleading answer, `typing` refuses: `isinstance(obj, P)` against an undecorated protocol raises `TypeError`, with a message stating that instance and class checks can only be used with `@runtime_checkable` protocols. ### What the decorator turns on `@typing.runtime_checkable` marks the protocol class as willing to be tested at runtime. From then on: * `isinstance(obj, P)` is legal, and returns True when the object has an attribute for every member *name* declared on the protocol. * `issubclass(C, P)` is legal too, but only when `P` declares methods and no data members. A protocol carrying an annotated data attribute still supports `isinstance()` while raising `TypeError` on `issubclass()`. The check is deliberately shallow. It compares names — never parameter lists, never return types, never the type of an attribute. A protocol requiring `def fetch(self, since: int) -> list[int]` is satisfied at runtime by any object carrying something called `fetch`, including an integer attribute of that name. ```python from typing import Protocol, runtime_checkable @runtime_checkable class Closable(Protocol): def close(self) -> None: ... class Weird: close = 3 isinstance(Weird(), Closable) # True — the name exists, that is all ``` ### How the lookup is performed Since Python 3.12 the check uses `inspect.getattr_static()` rather than `hasattr()`. `getattr_static` walks the class dictionaries and the instance `__dict__` without invoking the descriptor protocol and without calling `__getattr__`. Two consequences bite in real code. A `property` whose getter raises no longer produces a spurious False — and is never executed by the check, so a check can no longer trigger a side effect. Conversely, an object that fabricates attributes dynamically in `__getattr__` is now *not* accepted, even though `getattr()` on it would succeed; a proxy or lazy wrapper that passed the check on 3.11 can fail it on 3.12 and later. That is the one version-sensitive behaviour to state out loud in an interview. ### What the decorator does not change It does not affect static analysis at all: a type checker already validated structural conformance, including signatures, at every assignment and call site, decorated or not. It does not create an inheritance relationship, and it does not make the protocol instantiable. It only applies to protocol classes — decorating an ordinary class raises `TypeError` immediately, at class-creation time, not at first use. It also does not stop you from subclassing the protocol explicitly; a class that inherits from the protocol satisfies the check trivially and gets its conformance verified by the type checker at the class definition itself. ### Where the runtime check is worth using The honest use is a narrow one: validating an object at an input boundary so a caller gets a clear error immediately, rather than an `AttributeError` three frames deeper, and light branching over optional capabilities ("does this object expose a close method?"). Because the answer is presence-only, treat a True as *probably shaped correctly*, never as a guarantee that the object will behave. Anything stronger — argument counts, types, return values — has to come from the static type checker in CI, or from actually calling the object and handling the failure.

  • Does @typing.runtime_checkable change what a static type checker enforces?
    No. A type checker already validates structural conformance — member names, parameter lists and return types — at every assignment and call site, whether or not the protocol is decorated. The decorator only unlocks the runtime `isinstance()` and `issubclass()` paths, and the runtime check is far weaker than the static one. Adding it neither relaxes nor tightens static analysis.
  • What happens if you apply typing.runtime_checkable to a class that does not inherit from typing.Protocol?
    It raises `TypeError` immediately, while the module is being imported, saying the decorator can only be applied to protocol classes. That is a deliberate fail-fast: the decorator has nothing to attach to on a normal class, and silently doing nothing would leave a later `isinstance()` call answering a nominal question the author did not intend.
  • If a class explicitly inherits from a runtime-checkable protocol, does isinstance() still do the structural check?
    It does not need to. A real subclass satisfies the ordinary nominal check first, so the answer is True without inspecting attributes. Explicit inheritance also buys something the decorator cannot: the type checker verifies the implementation at the class definition itself, so a missing or mis-typed method is reported where the class is written rather than at a call site.

It is a guest list checked by first name only: the door opens for anyone whose name matches, and nobody verifies that the person can actually do the job they were invited for.

saying these in an interview costs you the question

  • Claiming isinstance works on any Protocol without the decorator
  • Saying the decorator makes the type checker stricter
  • Believing the runtime check compares method signatures
  • Thinking it can be applied to any ordinary class
  • Assuming it creates an inheritance relationship

context