Protocols you can check at run time
A Protocol describes methods; @runtime_checkable lets isinstance ask whether an object has them. It checks that the methods exist, not that they take the right arguments, which matters when you rely on it.
- Add
@runtime_checkableto a Protocol and test objects withisinstance - Explain what the check verifies (the methods exist) and what it doesn't (their parameters and results)
- Choose between an ABC (an is-a family) and a Protocol (anything with the right methods)
The Data Models & Typing module's Protocols were descriptions for readers and type checkers; isinstance(obj, SomeProtocol) raises TypeError. Put @runtime_checkable (from typing) above the Protocol and isinstance works: it is True when the object has every method the protocol lists, whatever its class and parents.
That makes it useful for capability checks: code that accepts plug-ins, or objects from several libraries, can ask *can this object quote a delivery price?* before using it.
Run each example to see what it prints. Running examples doesn't count toward progress.
from typing import Protocol, runtime_checkable @runtime_checkable class Carrier(Protocol): def quote(self, weight_kg: float) -> float: """Price to ship this weight.""" class RoyalPost: def quote(self, weight_kg: float) -> float: return 4.2 class FastFreight: def quote(self, weight_kg: float) -> float: return round(2.0 + 0.8 * weight_kg, 2) class Warehouse: def stock(self, sku: str) -> int: return 12 for thing in [RoyalPost(), FastFreight(), Warehouse(), "RoyalPost"]: print(type(thing).__name__, isinstance(thing, Carrier))
from typing import Protocol class Carrier(Protocol): def quote(self, weight_kg: float) -> float: """Price to ship this weight.""" class RoyalPost: def quote(self, weight_kg: float) -> float: return 4.2 print("checking") print(isinstance(RoyalPost(), Carrier))
The check has a limit worth knowing: it only looks for names. A render method that takes the wrong arguments, or returns the wrong type, still passes; so would an attribute that merely has the right name. The check tells you the object is *worth trying*, not that it will work. Combine it with error handling, or with a type checker that compares signatures before the program runs.
from typing import Protocol, runtime_checkable @runtime_checkable class Exporter(Protocol): def render(self, records: list[dict]) -> str: """Turn records into text.""" class CsvExporter: def render(self, records: list[dict]) -> str: return "\n".join(",".join(str(v) for v in r.values()) for r in records) class BrokenExporter: def render(self) -> str: return "nothing" class Notes: text = "remember to export" def load_plugins(candidates): accepted, rejected = [], [] for plugin in candidates: if isinstance(plugin, Exporter): accepted.append(type(plugin).__name__) else: rejected.append(type(plugin).__name__) return accepted, rejected print(load_plugins([CsvExporter(), BrokenExporter(), Notes()])) try: BrokenExporter().render([{"a": 1}]) except TypeError as err: print("TypeError:", err)
ABC or Protocol? An ABC is *nominal*: an object belongs only if its class inherits from the ABC, and the base can share code. A Protocol is *structural*: anything with the methods belongs, including classes you don't control. Use an ABC for your own family of classes with shared logic; use a Protocol to describe what a function needs from whatever it is given.
Mistakes to avoid: treating isinstance(x, SomeProtocol) as proof the methods behave correctly; long protocols that few objects satisfy; and run-time checks everywhere where plain duck typing (just call the method) would do.
@runtime_checkableletsisinstancetest a Protocol: True when every listed method name exists.- The check doesn't verify parameters or results; treat it as "worth trying".
- ABC: nominal family with shared code. Protocol: anything with the right methods.
- Next: small classes that add one behaviour each, combined with multiple inheritance.