Python Protocol Types: Duck Typing Done Right
You’ve probably written code that says, “If it has a .quack() method, I don’t care what it is.” That’s duck typing in action—Python’s love of behavior over class names. But traditional duck typing leaves type checkers like mypy scratching their heads. Enter Python Protocols: the tool that gives you duck typing with static type safety.
Protocols let you define a shape of behavior without forcing inheritance. If your object has the right methods and attributes, it’s an instance of the protocol—even if it’s from a completely unrelated class. This is structural subtyping, or “static duck typing,” and it’s a game-changer for clean, flexible, and type-safe Python code.
Why Duck Typing Needs a Type Check
Duck typing is powerful. You can pass any object to a function that expects a .read() method, and Python will happily call it. But here’s the catch:
def process(reader):
return reader.read()
process("not a reader") # Runtime error!
Type checkers can’t verify this until you run the code. That’s where Protocols step in. They let you say, “I expect anything with a .read() method that returns a string,” and mypy will catch mistakes before you run anything.
What Is a Protocol?
A Protocol is a special base class from the typing module that describes a contract of methods and attributes. It’s not enforced at runtime (unless you decorate it with @runtime_checkable), but it’s fully respected by static type checkers.
Here’s the core idea:
If an object has the required methods/attributes, it satisfies the protocol.
No inheritance. No base class drama. Just behavior.
Building Your First Protocol
Let’s say you’re working with different data sources: files, APIs, and in-memory buffers. They all have a .read() method, but they’re unrelated classes. Without protocols, you’d either:
- Force them to inherit from a common base (awkward), or
- Use duck typing and hope type checkers don’t complain.
With protocols, you define the shape once:
from typing import Protocol
class Readable(Protocol):
def read(self) -> str:
...
def process_source(source: Readable) -> None:
data = source.read()
print(f"Read {len(data)} characters")
class FileSource:
def read(self) -> str:
return "file content"
class ApiSource:
def read(self) -> str:
return "api response"
process_source(FileSource()) # ✅ mypy happy
process_source(ApiSource()) # ✅ mypy happy
Both FileSource and ApiSource satisfy Readable because they implement .read() returning a string. No inheritance. No shared base. Just behavior.
Adding Attributes and Multiple Methods
Protocols aren’t limited to methods. You can also specify attributes:
from typing import Protocol
class Configurable(Protocol):
name: str
timeout: int
def load(self) -> dict:
...
class MyConfig:
name = "app"
timeout = 30
def load(self) -> dict:
return {"setting": "value"}
def apply_config(cfg: Configurable) -> None:
print(f"Loading {cfg.name} with {cfg.timeout}s timeout")
data = cfg.load()
print(data)
apply_config(MyConfig()) # ✅
Now MyConfig must have name, timeout, and load(). If it misses any, mypy will flag it.
When to Use Protocols (and When Not To)
Use protocols when:
- You want duck typing but with static type safety.
- Your classes are unrelated but share behavior.
- You’re defining function parameters or generic constraints based on shape, not class.
Don’t use protocols when:
- You need runtime enforcement (unless you use
@runtime_checkable, which has caveats). - You’re already using a shared base class and inheritance suits your design.
- You’re writing simple scripts where type checking isn’t critical.
Making Protocols Runtime-Checkable (Optional)
By default, isinstance(obj, Protocol) always fails. But if you really need runtime checks:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Runnable(Protocol):
def run(self) -> None:
...
class Task:
def run(self) -> None:
print("Running...")
if isinstance(Task(), Runnable):
print("It's runnable!") # ✅
⚠️ Warning: @runtime_checkable only checks for the presence of methods/attributes, not their types or signatures. It’s a last resort, not a replacement for static checking.
Practical Tip: Use Protocols for Plugin Systems
One of the most powerful uses of protocols is plugin architecture. You define a protocol for what a plugin must do, and any class that matches becomes a valid plugin:
from typing import Protocol
class Plugin(Protocol):
name: str
def execute(self) -> None:
...
class LoggingPlugin:
name = "logger"
def execute(self) -> None:
print("Logging...")
class MonitoringPlugin:
name = "monitor"
def execute(self) -> None:
print("Monitoring...")
def run_plugin(p: Plugin) -> None:
print(f"Running {p.name}")
p.execute()
run_plugin(LoggingPlugin())
run_plugin(MonitoringPlugin())
Now you can add new plugins without touching the core system. Just implement Plugin, and you’re in.
Start Using Protocols Today
You don’t need to rewrite your entire codebase. Try this:
- Find a function that accepts “anything with a
.save()method.” - Define a
Saveableprotocol. - Add it as the type hint.
- Run
mypyand see what it catches.
from typing import Protocol
class Saveable(Protocol):
def save(self, path: str) -> bool:
...
def backup(obj: Saveable, path: str) -> None:
if obj.save(path):
print("Backup complete!")
You’ll get immediate feedback on missing or mismatched methods. That’s the power of static duck typing.
Wrap Up
Protocols give you the flexibility of duck typing with the safety of static types. They let you write code that’s polymorphic by behavior, not by inheritance. And they’re already in your typing module—no extra installs needed.
So next time you’re tempted to say, “It’s duck-typed, I’ll just check at runtime,” stop. Define a protocol instead. Your future self (and your type checker) will thank you.
Try it now: Pick one function in your code that uses duck typing, define a protocol for it, and add the type hint. Run mypy. See what you find.
Share your protocol wins (or fails) in the comments—I’d love to see what you build with them!
If you found this helpful, consider buying me a coffee ☕ — it keeps these articles coming!
Also check out my AI tools collection: AI 次元世界 — free AI tools for developers.
🛠️ Recommended Tool
If you found this useful, check out Content Creator Ultimate Bundle (Save 33%) — $29.99 and designed for developers like you.
Get instant access to our best-selling AI Dev Boost, HTML Landing Page Templates, AI Prompts for Developers, and Python Automation Scripts Pack, perfect for content creators and marketers looking to elevate their game. This bundle is a must-have for anyone looking to create stunning content, build high-converting landing pages, and drive real results. With these tools, you'll be able to create engaging content, build beautiful landing pages, and boost your online presence.
Top comments (0)