PYTHON

Python Protocol: Syntax, Usage, and Examples

A Python protocol is a type hint that describes a shape of behavior instead of a specific class.

It lets you say “anything with these methods and attributes is fine,” which fits perfectly with Python’s duck typing style.


How to Use Protocols in Python

Protocols live in the typing module (or typing_extensions in older versions). You define a base class that inherits from Protocol, list the methods and attributes you expect, then use that class as a type hint.

Basic Protocol Syntax

from typing import Protocol

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

Pieces to notice:

  • Protocol comes from typing.
  • Logger describes what a logger must provide, not how it works.
  • The method body uses ... (ellipsis) as a placeholder. You never instantiate Logger directly; you let real classes match it.

You then use the protocol in function annotations:

def process_data(logger: Logger, data: list[int]) -> None:
    logger.log(f"Processing {len(data)} items")
    # do something with data
    logger.log("Done")

Any object passed as logger only has to implement a compatible log() method. It doesn’t have to inherit from Logger.

Using Protocols With Existing Classes

You don’t need to change existing classes to “support” a protocol. The type checker cares about structure, not inheritance.

class ConsoleLogger:
    def log(self, message: str) -> None:
        print(message)

class FileLogger:
    def __init__(self, path: str) -> None:
        self.path = path

    def log(self, message: str) -> None:
        with open(self.path, "a", encoding="utf-8") as f:
            f.write(message + "\n")

# Both ConsoleLogger and FileLogger match Logger
process_data(ConsoleLogger(), [1, 2, 3])
process_data(FileLogger("app.log"), [4, 5, 6])

Static type checkers like mypy or Pyright will treat both classes as valid Logger implementations, because they match the protocol’s method signature.

Protocols With Attributes

Protocols can include attributes as well as methods.

from typing import Protocol

class UserLike(Protocol):
    username: str
    is_active: bool

def welcome(user: UserLike) -> str:
    if user.is_active:
        return f"Welcome back, {user.username}!"
    return "Please activate your account."

Any object with username and is_active attributes of the right types fits UserLike.

Generic Protocols

Sometimes you want a protocol that works with a type parameter, like a container that holds values of some type T.

from typing import Protocol, TypeVar, Iterable

T = TypeVar("T")

class SizedIterable(Protocol[T]):
    def __len__(self) -> int:
        ...
    def __iter__(self) -> Iterable[T]:
        ...

You can now write functions that accept anything iterable with a length, without restricting callers to a specific collection type.


When to Use Protocols in Python

You don’t need a protocol for every function. They shine in a few specific situations.

1. Duck-Typed Code That You Want to Type-Check

Python already encourages duck typing. You write functions like:

def send_all(clients, message):
    for client in clients:
        client.send(message)

This works at runtime for any client that has a send() method. A protocol lets you capture that assumption for the type checker.

from typing import Protocol, Iterable

class Sender(Protocol):
    def send(self, data: bytes) -> None:
        ...

def send_all(clients: Iterable[Sender], message: bytes) -> None:
    for client in clients:
        client.send(message)

Now tools can catch mistakes such as passing an object that doesn’t implement send().

2. Libraries and Plugins With Many Implementations

If you maintain a library, people might implement their own storage backends, loggers, or payment gateways. You care about the behavior, not the exact classes.

A protocol gives you a clear “contract”:

  • “Your class must have these methods.”
  • “If it does, my functions will work with it.”

This keeps extension points flexible and still lets users enjoy good type checking in their projects.

3. Testing and Mocking

Tests often use fake objects or mocks instead of real ones. A protocol defines the surface area your tests need.

You can:

  • Add fake implementations in tests that still match the protocol.
  • Spot tests that went out of sync with the real code when the protocol changes.

That beats chasing random AttributeError messages during test runs.

4. Refactoring Large Codebases

In big projects, several teams might implement similar concepts: clients, repositories, services, and so on. Defining a protocol early:

  • Documents expectations in one place.
  • Guides new code towards consistent interfaces.
  • Helps tools flag uses that no longer match the shared “contract” during refactors.

Examples of Python Protocols

Let’s make the idea more concrete with several examples you might bump into in real projects.

Example 1: A File-Like Protocol

Many functions just need “something file-like” that you can read from or write to.

from typing import Protocol

class FileLike(Protocol):
    def read(self, size: int = -1) -> str:
        ...
    def write(self, data: str) -> int:
        ...

Use it in a helper:

def copy_stream(src: FileLike, dst: FileLike) -> None:
    chunk = src.read()
    while chunk:
        dst.write(chunk)
        chunk = src.read()

You can now call copy_stream() with:

  • Real files from open()
  • io.StringIO buffers
  • Custom file-like objects that wrap APIs or network connections

The type checker confirms that your custom wrapper really behaves like a file.

Example 2: A Notification Service Protocol

Imagine a small app that sends notifications by email, SMS, or chat. Different teams may bring their own providers, but you want a consistent interface.

from typing import Protocol

class Notifier(Protocol):
    def send(
        self,
        recipient: str,
        subject: str,
        body: str,
    ) -> None:
        ...

def send_welcome(notifier: Notifier, email: str) -> None:
    notifier.send(
        recipient=email,
        subject="Welcome!",
        body="Thanks for signing up. Glad to have you here.",
    )

Two different implementations could look like this:

class EmailNotifier:
    def send(self, recipient: str, subject: str, body: str) -> None:
        # send email with SMTP or an API
        print(f"Email to {recipient}: {subject}")

class ChatNotifier:
    def send(self, recipient: str, subject: str, body: str) -> None:
        # send via chat integration
        print(f"Chat DM to {recipient}: {subject}")

Both classes work with send_welcome() without inheriting from anything special.

Example 3: A Sized Collection Protocol

Sometimes you only care that something has a length and can be iterated over, not what concrete container it is.

from typing import Protocol, Iterable, TypeVar

T = TypeVar("T")

class SizedCollection(Protocol[T]):
    def __len__(self) -> int:
        ...
    def __iter__(self) -> Iterable[T]:
        ...

Use it to calculate averages:

def average(values: SizedCollection[float]) -> float:
    total = 0.0
    count = 0
    for value in values:
        total += value
        count += 1
    if count == 0:
        raise ValueError("Cannot compute average of empty collection")
    return total / count

You can call average() with:

  • Lists: average([1.0, 2.5, 3.0])
  • Tuples: average((1.0, 2.0))
  • Custom classes that implement __len__() and __iter__()

Example 4: A Protocol for Context Managers

Context managers show up everywhere in Python code: files, database sessions, locks. A protocol can capture the basic pattern.

from typing import Protocol, TypeVar, ContextManager

T = TypeVar("T")

class SimpleContextManager(Protocol[T]):
    def __enter__(self) -> T:
        ...
    def __exit__(self, exc_type, exc, tb) -> bool | None:
        ...

Use it in helper functions:

def use_resource(manager: SimpleContextManager[str]) -> str:
    # manager could open a file, create a session, etc.
    with manager as resource:
        return resource.upper()

Any custom context manager that follows this pattern can be plugged in and type-checked.


Learn More About Protocols in Python

Once you’re comfortable with the basics, a few deeper ideas help you use protocols in a more confident way.

Structural Typing vs. Inheritance

Traditional object-oriented design often focuses on explicit inheritance:

  • “Class B is-a A, so it subclasses A.”

Protocols embrace structural typing instead:

  • “Class B behaves like this protocol, so it can stand in for anything expecting that protocol.”

You don’t have to rearrange your inheritance tree to reuse code. You describe behavior in one place and let any matching class participate.

Protocols vs. Abstract Base Classes (ABCs)

Abstract base classes and protocols sometimes look similar, but they serve slightly different goals.

Abstract base classes:

  • Often enforce a contract at runtime through inheritance.
  • Can provide default method implementations.
  • Sometimes register virtual subclasses.

Protocols:

  • Focus on static type checking rather than runtime checks.
  • Don’t need classes to inherit from them.
  • Play nicely with existing duck-typed code.

You can mix both approaches: keep ABCs for runtime behavior when you truly need it, and use protocols for flexible type hints.

Checking Protocols at Runtime

In some cases you want to ask at runtime: “Does this object match the protocol?” Python lets you do this for selected protocols using @runtime_checkable.

from typing import Protocol, runtime_checkable

@runtime_checkable
class JSONSerializable(Protocol):
    def to_json(self) -> str:
        ...

def debug(obj: object) -> None:
    if isinstance(obj, JSONSerializable):
        print(obj.to_json())
    else:
        print(repr(obj))

A few notes:

  • Runtime checks work for instance attributes and methods, not every subtle type detail.
  • They are best used sparingly, usually in debugging, logging, or validation paths.

Combining and Extending Protocols

You can build bigger protocols by extending smaller ones. That keeps definitions clear and reduces duplication.

class Reader(Protocol):
    def read(self, size: int = -1) -> str:
        ...

class Writer(Protocol):
    def write(self, data: str) -> int:
        ...

class ReadWriter(Reader, Writer, Protocol):
    ...

Now any class that has read() and write() methods automatically matches ReadWriter in the eyes of the type checker.

Version Notes and typing_extensions

Protocols became part of the standard typing module in modern Python versions. In older 3.x versions, they arrived first through the typing_extensions package.

If you maintain code that supports an older Python release, you can write:

try:
    from typing import Protocol
except ImportError:
    from typing_extensions import Protocol

This keeps type hints consistent while staying compatible with various Python versions.

How Protocols Change Your Thinking

Once you start using protocols, you may notice a small mindset shift:

  • Instead of thinking, “All these classes must inherit from BaseClient,”
  • You think, “All these classes must offer connect(), send(), and close().”

This approach often leads to code that is easier to test, easier to swap out, and less entangled with concrete inheritance chains.


Summary

A Python protocol describes behavior in terms of methods and attributes instead of specific classes. You define a class that inherits from Protocol, list the operations that matter, and then use that class as a type hint. Any object that looks like the protocol can be used where the protocol is expected.

Protocols shine when you already use duck typing and want stronger type checking without sacrificing flexibility. They help with plugins, testing, refactoring, and large projects that rely on consistent “contracts” between parts of the codebase.

Learn to Code in Python for Free
Start learning now
button icon
To advance beyond this tutorial and learn Python by doing, try the interactive experience of Mimo. Whether you're starting from scratch or brushing up your coding skills, Mimo helps you take your coding journey above and beyond.

Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.