runtime
142 / 250
lesson 142 Advanced OOP & Design · advanced

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.

You'll be able to
  • Add @runtime_checkable to a Protocol and test objects with isinstance
  • 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 idea

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.

example-1.py · Which objects can quote shipping?
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))
example-2.py · isinstance on a plain Protocol
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.

example-3.py · Loading export plug-ins
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.

Review
  • @runtime_checkable lets isinstance test 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.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Protocols you can check at run time — Advanced OOP & Design · Runtime