Codectionary / Developer documentation / Python

Protocol

Describe an interface by the operations an object provides, without forcing implementations to inherit a shared base class.

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())
  1. Friendly does not inherit Greeter, but supplies the declared method. welcome calls that method normally; structural compatibility is a static-checking concept.
  2. Use this boundary to swap greeting implementations without changing the consumer.
  3. A matching method name alone is insufficient for full static compatibility: parameter and return types also matter.
  4. 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()))
  1. report depends only on count. FixedCounter returns a stable value, so the same consumer can be exercised without a database or external service.
  2. Use a small protocol to make a reporting function independently testable.
  3. The fake proves this consumer interaction, not the correctness of a real storage implementation.
  4. 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))
  1. The decorator allows the two runtime checks. Resource exposes close, whereas a plain object does not, so the results differ.
  2. Use a runtime protocol check only when a shallow capability check is sufficient.
  3. This check does not prove that close accepts the declared arguments, returns None, or behaves correctly.
  4. 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

Specifications & further reading