Command Palette

Search for a command to run...

Hectal
PHASE 10Intermediate ~26 min· topic 6 of 9

Topic 10.6

Protocols and Structural Typing

In one line

A Protocol describes the methods and attributes an object must have, and anything that has them qualifies, with no inheritance. It is duck typing that a checker can verify: "if it quacks like this, it fits".

Think of it like this

A power socket. The socket does not care about the brand of your charger. If the plug has the right shape, it fits. A Protocol describes the shape of the plug: any object whose methods fit the shape is accepted, without a shared parent class or a registration step.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Duck typing
Judging an object by what it can do (its methods), not by its class name.
Structural typing
Compatibility decided by the shape of an object (the methods and attributes it has).
Nominal typing
Compatibility decided by declared names: the class must inherit from the named class.
Protocol
A class that lists the methods and attributes an object needs to match.
`runtime_checkable`
A decorator that lets isinstance work with a Protocol (it only checks that names exist).
Interface
The set of methods other code is allowed to rely on.
Test double (fake)
A simple stand-in object used in tests in place of a real one.

Step by step

01Describe the shape

A Protocol lists the methods. The ... (three dots, called Ellipsis) is the body: it means "nothing to put here". Protocols are never meant to be used as real classes.

main.pywhole filepython
from typing import Protocol

class Storage(Protocol):
    def save(self, key: str, value: str) -> None: ...
    def load(self, key: str) -> str: ...

02Any matching class fits, with no inheritance

MemoryStorage and FakeStorage below never mention Storage. They have save and load with matching signatures, so they fit. The function remember asks only for the shape it uses. Run the second example.

03What the checker does

When you call remember(Dog(), "x") and Dog lacks save, the checker prints the error with the missing member. It compares method names, parameter types and return types. A method that accepts everything the Protocol's version accepts (or more) is fine.

04Run-time checks: only names

With @runtime_checkable, isinstance(obj, Speaker) is True when obj has an attribute called speak. The first example proves the weakness: WrongSpeaker.speak needs an extra argument, yet it passes. If the shape matters at run time, test it by calling, or validate with Pydantic.

05Protocol or ABC?

Use an ABC when you own the family of classes, want to share code in the base, and want a clear error when an abstract method is missing (Can't instantiate abstract class ...). Use a Protocol when callers bring their own classes, when you cannot or do not want to force inheritance, or in tests. Standard-library ones already exist: collections.abc.Iterable, Sized, Callable are protocols in spirit.

06Protocols for LLM clients

Define class ChatModel(Protocol): def complete(self, prompt: str) -> str: ... and write your pipeline against that. In production pass an object wrapping a real SDK. In tests pass a three-line fake that returns a fixed reply. Your tests become fast and free, and swapping providers touches one class.

Try it yourself

  1. 1

    Make something fit

    In the first example, add a class Parrot with a speak that returns "Hello" and pass it to announce. It works with no change to Speaker.

  2. 2

    Write a fake model

    Define class ChatModel(Protocol): def complete(self, prompt: str) -> str: ..., a class FakeModel that returns "fixed answer", and a function summarise(model: ChatModel, text: str) -> str. Call it with the fake.

Code & diagrams

runtime_checkable only checks names New tab

Dog never inherits Speaker. WrongSpeaker has a different signature yet passes isinstance, because only the name speak is tested.

Sign in to run this example in your browser.

Expected output

announce: Woof
Dog is a Speaker: True
Rock is a Speaker: False
WrongSpeaker is a Speaker: True
issubclass(Dog, Speaker): True
AttributeError: 'Rock' object has no attribute 'speak'
A real class and a test fake, one Protocol New tab

Neither storage class inherits Storage. The ABC at the end shows the other style: inheritance required, error at creation time.

Sign in to run this example in your browser.

Expected output

Asha
fake ['save name', 'load name']
ABC: Can't instantiate abstract class Impl without an implementation for abstract method 'save'
Nominal versus structuraldiagram
Rendering diagram…

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

isinstance on a Protocol that is not runtime_checkable

Remove the @runtime_checkable line from the Speaker Protocol and test with isinstance.

main.pywhole filepython
from typing import Protocol

class Speaker(Protocol):
    def speak(self) -> str: ...

class Dog:
    def speak(self) -> str:
        return "Woof"

print(isinstance(Dog(), Speaker))
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 10, in <module>
print(isinstance(Dog(), Speaker))
~~~~~~~~~~^^^^^^^^^^^^^^^^
File "/lib/python313.zip/typing.py", line 2117, in __instancecheck__
raise TypeError("Instance and class checks can only be used with"
" @runtime_checkable protocols")
TypeError: Instance and class checks can only be used with @runtime_checkable protocols

Myth vs fact

Myth

A class must inherit from a Protocol to implement it.

Fact

No. Matching the shape is enough; that is the whole point. You may still inherit explicitly if you want the checker to verify your class against it right at the class definition.

Myth

isinstance with a Protocol checks the method signatures.

Fact

It only checks that the attribute names exist. The example class with the wrong signature passes.

Myth

Protocols replace abstract base classes.

Fact

They solve different problems. ABCs share code and enforce implementation at creation time; Protocols describe what a caller needs without any inheritance.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Keep Protocols small and owned by the consumer (the code that calls the methods), not the provider. A one- or two-method Protocol ("interface segregation") is easy to satisfy and easy to fake. This is the Python form of dependency inversion from Topic 8.11.

  • ▸

    Protocols can include attributes and properties, and can be generic (class Source[T](Protocol): def get(self) -> T: ...). The standard library's SupportsInt, SupportsIndex and Hashable are protocols; _typeshed has more for checkers.

  • ▸

    A class can inherit explicitly from a Protocol to get the checker to verify it at the definition site (and to inherit default methods). It then behaves as a normal class.

  • ▸

    Run-time checks with runtime_checkable are slower than a normal isinstance (they look up every member of the Protocol), although Python 3.12 made them much faster. Since 3.12 they inspect members with inspect.getattr_static, so a property is not executed by the check (before 3.12 it was, through hasattr, which could run code or raise). Do not use them in hot loops.

  • ▸

    A Protocol with data members (such as name: str) can be used with isinstance when it is runtime_checkable, but issubclass raises TypeError: Protocols with non-method members don't support issubclass(), because a class alone does not show instance attributes. Only the names are tested, never the types.

  • ▸

    A Callable hint cannot express keyword-only parameters or defaults. A callback protocol can: class Handler(Protocol): def __call__(self, event: str, *, retries: int = 3) -> bool: .... Any function or object with a compatible call signature fits, and mismatched keyword names are reported. Also note that Protocol members are compared with the normal rules (parameters contravariant, return types covariant), and a mutable attribute in a Protocol must match exactly, so prefer a read-only @property in the Protocol when you only read the value.

Remember this

  1. 1

    Python has always worked this way: duck typing ("if it walks like a duck and quacks like a duck, it is a duck"). A function that calls obj.read() works with any object that has a read() method, whether it is a file, a web download or a stand-in object used in tests. What was missing is a way to *write that down* so a checker can verify it. A Protocol is that way.

  2. 2

    You write a class that inherits from Protocol (Topic 8.5 explains inheriting) and lists the required methods, using ... as the body. For example, a Speaker protocol has one method speak(self) -> str.

  3. 3

    A function parameter hinted Speaker then accepts every object with a compatible speak method. This is called structural typing: compatibility is decided by structure (what the object has), not by name (what it inherits from).

  4. 4

    Compare it with an abstract base class (ABC, Topic 8.10). With an ABC the implementer must inherit it: class Dog(Animal). With a Protocol the implementer does not even need to know the Protocol exists.

  5. 5

    That matters in three places. First, for code you cannot edit, such as a class from a library someone else wrote. Second, for tests, where you pass a tiny fake object. Third, for keeping parts of a program independent: the code that uses a database can define the little interface it needs, and any storage class that happens to match will work.

  6. 6

    Protocols are checked by the type checker, before running. If you pass an object that lacks the method, mypy reports it (the checker session in Topic 10.3 shows the Dog that has bark but no quack). At run time Python does nothing, as always.

  7. 7

    If you do want isinstance to work, add @runtime_checkable. But be careful: the run-time check only tests that the names exist. It does not check parameters or return types, so a class whose speak takes different arguments still passes isinstance, as the example shows. Use it for simple tests, not as validation.

  8. 8

    Protocols are also how you describe "anything with a .read()" or "anything I can compare with <" without naming a class, and how AI libraries type pluggable pieces such as an LLM client (the object that talks to a language model), a vector store (a database of embeddings, Topic 16.8) or a tool. A test then passes a tiny FakeClient that implements the three methods your code needs, and no real network is touched.

Explain it without notes

01

What is structural typing, and how does a Protocol give it to Python?

02

How does a Protocol differ from an abstract base class? When do you pick each?

03

What does @runtime_checkable let you do, and what is the catch?

04

Why do Protocols make code easier to test?

05

A function accepts a Handler Protocol with __call__. When would you choose that over Callable[[str], bool]?

Practice

01

Write a Protocol Greeter with greet(self, name: str) -> str and a class that satisfies it. Write welcome(g: Greeter) that prints g.greet("Asha").

02

Write a Protocol HasLength with __len__, and a function that returns "empty" or "not empty" for any object with a length. Call it with a list and a string.

03

Write a Protocol Clock with now(self) -> str and a FixedClock for tests that always returns "12:00".

Trade-offs

  • ↔

    Flexibility versus discoverability. With no inheritance there is no "find all implementations" in the editor. Name Protocols clearly and keep them near their consumer.

  • ↔

    Static only. A Protocol protects you at check time; if the check never runs, a wrong class fails with AttributeError at run time.

  • ↔

    ABC for sharing, Protocol for decoupling. Mixing both in one hierarchy is possible but usually a sign the design should pick one.

Done when you can

  • Done when you can write a Protocol and a class that satisfies it without inheriting.

  • Done when you can explain structural versus nominal typing.

  • Done when you can say why isinstance with runtime_checkable is shallow.

  • Done when you can choose between a Protocol and an ABC for a case.

  • Done when you can sketch a Protocol-based fake for testing.