Python

Python Protocols👨‍💻

Python has always embraced duck typing: if an object has a .read() method, you can pass it anywhere that calls .read(), regardless of its class hierarchy. The problem is that type checkers and IDEs have no way to verify this at development time. You end up with Any everywhere, or you force classes to inherit from an ABC they shouldn't know about.

typing.Protocol (PEP 544, Python 3.8+) fixes this. A Protocol defines a structural contract -- a set of methods and attributes a type must have -- without requiring inheritance. Think of it as Python's answer to TypeScript interfaces or Go's implicit interfaces. Any class that has the right shape satisfies the Protocol automatically. You get the flexibility of duck typing with the safety of static analysis, and you never have to modify third-party code to make it conform.

Key Takeaways

  • 1Protocols enable structural subtyping: a class satisfies a Protocol if it has the required methods and attributes, with no inheritance needed
  • 2Unlike ABCs, Protocols don't force coupling -- you can define a Protocol for third-party classes you don't control, and they'll satisfy it as-is
  • 3The `@runtime_checkable` decorator lets you use `isinstance()` checks against a Protocol, but it only verifies method signatures, not attribute types or return types
  • 4Protocols support properties, class variables, generic type parameters, and composition through multiple inheritance
  • 5Protocols and ABCs solve different problems: use Protocols for decoupling and external code, ABCs when you need shared implementation or want to enforce inheritance
  • 6Protocols are the idiomatic way to type-hint callback signatures, plugin interfaces, and repository abstractions in modern Python

Master python protocols

Take the Python Architecture course with hands-on lessons and challenges.

Examples

Repository protocol -- database abstraction without coupling

python

The service depends on the Protocol, not a concrete class. PostgresUserRepo and InMemoryUserRepo both satisfy UserRepository because they have matching methods. Neither knows the Protocol exists. This is the core value: you decouple producers from consumers without inheritance chains.

Serializable protocol -- typing third-party objects

python

Protocols compose naturally through multiple inheritance. Order satisfies both Serializable and Persistable without knowing about either. You can define narrow protocols (Serializable) for functions that only need one capability, and wider ones (Persistable) when you need multiple.

runtime_checkable -- plugin discovery at runtime

python

The @runtime_checkable decorator enables isinstance() checks against Protocols. This is useful for plugin architectures where you discover objects dynamically and need to filter them at runtime. Keep in mind that isinstance only checks for method existence, not return types or attribute types -- static type checking is still the primary safety net.

Protocols with properties and generics

python

Protocols can include @property definitions and generic type parameters. Here, Repository[T] constrains T to types satisfying Identifiable. InMemoryProductRepo matches the Repository[Product] shape, and Product matches Identifiable through its id property. This is the foundation pattern for clean architecture in Python.

Protocol vs ABC -- when each one wins

python

ABCs are the right choice when subclasses share implementation (here, logging). Protocols are the right choice when you need a common interface across unrelated classes, especially third-party ones. In practice, you often use both: ABCs within your codebase for implementation reuse, Protocols at the boundary for flexibility.

Common Mistakes

Mistake:

Inheriting from a Protocol in the implementing class -- treating it like an ABC

Fix:

Don't inherit from the Protocol unless you're composing Protocols together. The whole point of structural subtyping is that implementers don't need to know about the Protocol. If you write `class MyRepo(UserRepository):`, you've defeated the purpose. Just implement the methods and let the type checker verify the match.

Mistake:

Using `@runtime_checkable` and assuming isinstance checks full type safety -- it only checks method existence

Fix:

isinstance(obj, MyProtocol) verifies that the object has the right method names, but it does not check argument types, return types, or attribute types. For full safety, rely on static type checking (mypy/pyright). Use @runtime_checkable for coarse filtering like plugin discovery, not as a substitute for proper type analysis.

Mistake:

Defining overly broad Protocols with many methods, making them hard to satisfy

Fix:

Follow the Interface Segregation Principle. Split large Protocols into small, focused ones: Reader, Writer, Closer rather than one giant IOStream Protocol. Functions should depend on the narrowest Protocol they actually need. Compose them when a function truly requires multiple capabilities.

Mistake:

Forgetting `Protocol` in the base classes when composing Protocols -- `class ReadWriter(Reader, Writer)` silently creates a regular class

Fix:

When combining Protocols, always include Protocol in the inheritance list: `class ReadWriter(Reader, Writer, Protocol)`. Without it, you get a concrete class that happens to inherit from two Protocols, which is not the same thing as a combined Protocol.

Best Practices

  • Keep Protocols small and focused -- one Protocol per capability (Readable, Writable, Closable) rather than one monolith. This follows the Interface Segregation Principle and maximizes the number of types that naturally satisfy each Protocol.
  • Use Protocols at module boundaries and for dependency injection. Your service should accept a `UserRepository` Protocol, not a `PostgresUserRepo` class. This makes testing trivial and keeps your architecture flexible.
  • Prefer static type checking over @runtime_checkable. Run mypy or pyright in CI to catch Protocol mismatches at development time. Reserve runtime checks for genuinely dynamic scenarios like plugin loading.
  • Document what the Protocol expects with docstrings on each method. Since implementers won't see your Protocol class (that's the point), the docstring is how you communicate the contract to future maintainers.
  • Use Protocols instead of ABCs when the implementing class is outside your control -- third-party libraries, standard library types, or code owned by another team. ABCs require inheritance; Protocols don't.

Summary

Protocols bring static type safety to Python's duck typing philosophy. They define structural contracts -- methods and attributes a type must have -- without requiring inheritance. Any class with the right shape satisfies the Protocol automatically. Use @runtime_checkable for dynamic scenarios like plugin discovery, compose small Protocols for flexibility, and prefer Protocols over ABCs when you need to type external or third-party code. In practice, Protocols are the tool that lets you write type-safe Python without giving up the loose coupling that makes Python productive.

Practice Python with hands-on challenges

Learn python protocols hands-on in your IDE

Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.

Related Concepts

Related Cheatsheets

Master Python with Stanza

Interactive lessons and challenges, right in your code editor.

Check the free courses. No credit card.