Protocols for Structural Typing

RoadmapsPython

Overview

In Python type hints, a `Protocol` defines a set of methods or attributes that an object must have, without requiring the object to inherit from a base class. It formalizes duck typing. For example, if you define a `Serializer` protocol with a `dump()` method, any class that implements `dump()` is considered a valid `Serializer` by the type checker, even if it doesn't subclass `Serializer`. from typing import Protocol, Any class Serializer(Protocol): def dump(self, obj: Any) -> str: ... class JSONSerializer: def dump(self, obj: Any) -> str: import json return json.dumps(obj) The mental model is "shape-matching": the type checker verifies if an object's "shape" (its methods and attributes) matches the `Protocol`'s required shape. It's an analogy to checking if a plug fits a socket—the brand of the plug doesn't matter, only the pin layout. def export_data(data: Any, serializer: Serializer) -> str: return serializer.dump(data) export_data({"id": 1}, JSONSerializer()) # Valid shape A sharp edge occurs when using `@runtime_checkable` for runtime `isinstance()` checks. The runtime check only verifies that the method names exist, ignoring parameter types and return types. from typing import runtime_checkable @runtime_checkable class Serializer(Protocol): def dump(self, obj: Any) -> str: ... class BrokenSerializer: def dump(self) -> None: pass isinstance(BrokenSerializer(), Serializer) # Returns True! Static type checkers will catch the signature mismatch, but runtime checks will blindly pass.

`Protocol`s allow you to write strongly typed code without coupling classes together through inheritance, leading to cleaner and more flexible designs.

Where used: Dependency Injection, Abstracting external libraries, Mocking in tests

Why learn this

Code walkthrough

from typing import Protocol

class Logger(Protocol):
    def log(self, msg: str) -> None: ...

class ConsoleLogger:
    def log(self, msg: str) -> None:
        print(f'Log: {msg}')

def process(logger: Logger) -> None:
    logger.log('Started')

process(ConsoleLogger())

Focus: def process(logger: Logger) -> None:

Aha moment

from typing import Protocol, runtime_checkable

@runtime_checkable
class Swimmer(Protocol):
    def swim(self) -> None: ...

class Fish:
    def swim(self) -> None: pass

print(issubclass(Fish, Swimmer))

Prediction: What does `issubclass()` return?

Common guess: `False`, because `Fish` does not inherit from `Swimmer`.

It prints `True`. When a `Protocol` is decorated with `@runtime_checkable`, `issubclass()` structurally checks if `Fish` has the required methods, bypassing normal inheritance checks.

Common mistakes

Glossary

duck typing
A programming concept where an object's suitability is determined by its methods and properties rather than its explicit class type. Example: `obj.quack()`.
structural subtyping
A type system where a variable's type is determined by the methods and properties it has, rather than its explicit class name. Example: `class Quacker(Protocol):`.

Recall questions

Understanding checks

What does this code print, and why?

`True`

The `Duck` class implements the `quack()` method, matching the `Quacker` `Protocol`. Because `Quacker` is decorated with `@runtime_checkable`, `isinstance()` structurally checks `Duck` at runtime.

Does a type checker raise an error for passing `User()` to `save_entity()`?

`No`

`User` implements the `save()` method required by the `Saver` `Protocol`. Even though `User` doesn't inherit from `Saver`, the type checker recognizes it as a valid `Saver`.

Practice tasks

Define a Protocol for rendering

Given this function that expects an object with a `render() -> str` method, define a `Renderer` `Protocol` and use it as the type hint for the `obj` parameter.

Challenge

Mocking with Protocols

Create a `Notifier` `Protocol` with a `notify(msg: str)` method. Create an `EmailNotifier` class that implements it. Create a `send_alert` function taking a `Notifier` and calling `notify('Alert!')`.

Continue learning

Previous: Duck Typing

Return to Python Roadmap