- __init__() function
- Aliases
- and operator
- argparse
- Arrays
- Booleans
- Break statement
- Bytes
- Classes
- Closure
- Code blocks
- Comments
- Conditional statements
- Console
- Context manager
- Data class
- Data structures
- Data visualization
- datetime module
- Decorator
- Dictionaries
- Django
- Docstrings
- Encapsulation
- enum
- enumerate() function
- Equality operator
- Error handling
- Exception handling
- False
- File handling
- Filter()
- Flask framework
- Floats
- Floor division
- For loops
- Formatted strings
- Functions
- Generator
- Globals()
- Greater than operator
- Greater than or equal to operator
- If statement
- in operator
- Indices
- Inequality operator
- Inheritance
- Integers
- Iterator
- Lambda function
- len() Function
- Less than operator
- Less than or equal to operator
- List append() method
- List comprehension
- List count()
- List insert() method
- List pop() method
- List reverse() method
- List sort() method
- Lists
- Logging
- map() function
- Match statement
- Math module
- Merge sort
- Min()
- Modules
- Modulo operator
- Multiline comment
- Multiprocessing
- Multithreading
- None
- not operator
- NumPy library
- OOP
- or operator
- Override method
- Pandas library
- Parameters
- pathlib module
- Pickle
- Polymorphism
- print() function
- Property()
- Protocol
- Random module
- range() function
- Raw strings
- Recursion
- Reduce()
- Regular expressions
- requests Library
- return statement
- round() function
- Script
- Sets
- SQLite
- String decode()
- String find()
- String join() method
- String replace() method
- String split() method
- String strip()
- Strings
- Ternary operator
- time.sleep() function
- True
- try...except statement
- Tuples
- Type casting
- Variables
- Virtual environment
- What is Python?
- While loops
- Zip function
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.
Learn Python on Mimo
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
Python
from typing import Protocol
class Logger(Protocol):
def log(self, message: str) -> None:
...
Pieces to notice:
Protocolcomes fromtyping.Loggerdescribes what a logger must provide, not how it works.- The method body uses
...(ellipsis) as a placeholder. You never instantiateLoggerdirectly; you let real classes match it.
You then use the protocol in function annotations:
Python
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.
Python
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.
Python
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.
Python
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:
Python
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.
Python
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.
Python
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:
Python
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.StringIObuffers- 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.
Python
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:
Python
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.
Python
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:
Python
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.
Python
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:
Python
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
Bis-aA, so it subclassesA.”
Protocols embrace structural typing instead:
- “Class
Bbehaves 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.
Python
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.
Python
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:
Python
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(), andclose().”
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.
Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.