Understand the contract
A Protocol is a structural interface for static typing. A class satisfies it by providing compatible attributes and methods, even if the class never names the protocol as a base. This supports duck typing while letting a checker verify the expected operations.
Place only the operations the consumer needs in the protocol. A small read interface can accept many unrelated implementations, making adapters and test doubles easier to write. Protocol inheritance is optional for implementations.
Follow the runtime behavior
The consumer executes ordinary method calls. No automatic conversion or wrapper is introduced by the parameter annotation. A missing method still causes an ordinary runtime error if static checking was skipped.
runtime_checkable permits limited isinstance checks for attribute presence. It does not verify full method signatures or return types and is not a substitute for validating an object. Protocol annotations do not enforce runtime behavior.
Apply it and practise
Use structural interfaces at boundaries where multiple implementations should be interchangeable. Prefer a concrete type when there is only one implementation and no useful abstraction to describe.
Exercise: define a protocol with save(text: str) -> None. Implement an in-memory saver with a list, pass it to a function annotated with the protocol, and verify the stored text. Then explain what a checker should report for a class missing save.
Syntax
class Named(Protocol):
def name(self) -> str: ...Examples
Accept an independent implementation
Use this boundary to swap greeting implementations without changing the consumer.
from typing import Protocol
class Greeter(Protocol):
def greet(self) -> str: ...
class Friendly:
def greet(self) -> str:
return "Hello"
def welcome(person: Greeter) -> None:
print(person.greet())
welcome(Friendly())- Friendly does not inherit Greeter, but supplies the declared method. welcome calls that method normally; structural compatibility is a static-checking concept.
- Use this boundary to swap greeting implementations without changing the consumer.
- A matching method name alone is insufficient for full static compatibility: parameter and return types also matter.
- Expected output: Hello
Replace a dependency with a fake
Use a small protocol to make a reporting function independently testable.
from typing import Protocol
class Counter(Protocol):
def count(self) -> int: ...
class FixedCounter:
def count(self) -> int:
return 7
def report(source: Counter) -> str:
return f"Items: {source.count()}"
print(report(FixedCounter()))- report depends only on count. FixedCounter returns a stable value, so the same consumer can be exercised without a database or external service.
- Use a small protocol to make a reporting function independently testable.
- The fake proves this consumer interaction, not the correctness of a real storage implementation.
- Expected output: Items: 7
Inspect runtime shape
Use a runtime protocol check only when a shallow capability check is sufficient.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Closable(Protocol):
def close(self) -> None: ...
class Resource:
def close(self) -> None:
pass
print(isinstance(Resource(), Closable))
print(isinstance(object(), Closable))- The decorator allows the two runtime checks. Resource exposes close, whereas a plain object does not, so the results differ.
- Use a runtime protocol check only when a shallow capability check is sufficient.
- This check does not prove that close accepts the declared arguments, returns None, or behaves correctly.
- Expected output: True False
Best practices
- runtime_checkable permits limited isinstance checks for attribute presence. It does not verify full method signatures or return types and is not a substitute for validating an object. Protocol annotations do not enforce runtime behavior.
- Use structural interfaces at boundaries where multiple implementations should be interchangeable. Prefer a concrete type when there is only one implementation and no useful abstraction to describe.
Runtime & versions
Python interpreter. Feature availability depends on the language and runtime version used by your project; consult the official reference below.
Python language reference