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.
Master python protocols
Take the Python Architecture course with hands-on lessons and challenges.
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.
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.
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 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.
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.
Inheriting from a Protocol in the implementing class -- treating it like an ABC
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.
Using `@runtime_checkable` and assuming isinstance checks full type safety -- it only checks method existence
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.
Defining overly broad Protocols with many methods, making them hard to satisfy
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.
Forgetting `Protocol` in the base classes when composing Protocols -- `class ReadWriter(Reader, Writer)` silently creates a regular class
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.
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.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.