Introduction
In our last lesson, you mastered the fundamentals of annotating Python code using basic types like list, dict, Optional, Union, and Callable. You now have the tools to make function signatures explicit, which improves readability and enables static analysis.
This lesson addresses a core concept that directly bridges your Java background with Python's unique philosophy: defining interfaces. In Java, you use the interface keyword to enforce a contract through explicit implementation. Python, which favors duck typing ("if it walks like a duck and quacks like a duck, then it must be a duck"), takes a different approach.
Today, you will learn how to use typing.Protocol to define interfaces for duck-typed objects without requiring inheritance. A Protocol formalizes this "duck test" for static type checkers like mypy. It allows you to specify what an object can do (its behavior and attributes) rather than what it is (its class hierarchy). This is a powerful way to write flexible, decoupled code that remains type-safe.
Structural vs. Nominal Typing: The Core Idea
The distinction between Protocol and a Java interface or a Python Abstract Base Class (ABC) comes down to two different ways of thinking about types:
-
Nominal Typing: Type compatibility is determined by explicit declaration. If class
Binherits from classA, thenBis a subtype ofA. This is the world ofclass MyClass(MyParent):and Java'sclass MyClass implements MyInterface. The name and declared hierarchy matter. -
Structural Typing: Type compatibility is determined by structure. If class
Bhas the same methods and attributes as defined by an "interface"A, then it's compatible, regardless of whether it explicitly inherits fromA. This is the formal name for duck typing. The structure and behavior matter.
typing.Protocol is Python's tool for structural typing.

To solidify this foundational concept, the following article provides an excellent explanation.
Python Protocols: Leveraging Structural Subtyping
The Real Python article 'Python Protocols' provides a clear breakdown of structural vs. nominal subtyping. Understanding this is key to grasping the power of Protocol.
Read the introduction and the section titled 'Structural Subtyping and Protocols'. Focus on how the Dog and Cat classes can be used interchangeably in animals_v2.py because they share a common structure, even without a common parent class.
Defining and Using a Protocol
You define a protocol by inheriting from typing.Protocol. Inside the protocol class, you define the methods and attributes that any conforming object must have. Method bodies in a protocol should be empty, typically indicated with ....
Let's look at a classic example. Suppose we want to work with any object that has an area.
from typing import Protocol
import math
# 1. Define the Protocol
class HasArea(Protocol):
"""A protocol for objects that have an area."""
def area(self) -> float:
...
# 2. Define conforming classes (NO INHERITANCE from HasArea)
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Circle:
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
# 3. Use the Protocol in a function signature
def print_area(shape: HasArea):
"""Prints the area of any object that conforms to the HasArea protocol."""
# Mypy knows that `shape` has an `area()` method because of the type hint.
print(f"The area is: {shape.area():.2f}")
# 4. Use with conforming objects
rect = Rectangle(10, 5)
circ = Circle(7)
print_area(rect) # Mypy passes this: Rectangle has an area() method.
print_area(circ) # Mypy passes this: Circle has an area() method.
# What if we pass a non-conforming object?
class Triangle:
def __init__(self, base: float, height: float):
self.base = base
self.height = height
# No area() method!
def calculate_area(self) -> float:
return 0.5 * self.base * self.height
tri = Triangle(10, 5)
# print_area(tri)
# ^-- If you uncomment this and run mypy, it will raise an error:
# error: Argument 1 to "print_area" has incompatible type "Triangle"; expected "HasArea"
# note: "Triangle" is missing "area"
Notice the key difference from Java: Rectangle and Circle don't mention HasArea anywhere. They conform implicitly because they have the right structure. This is extremely powerful for working with code from third-party libraries where you cannot modify the class definitions to add a base class.
Practical Example: Decoupling Code with Protocols
One of the most significant benefits of protocols is decoupling your application's logic from concrete implementations. This makes your code more flexible, extensible, and easier to test.
The following video demonstrates a classic "bad vs. good" refactoring. It starts with a function tightly coupled to a specific email service and refactors it to use a protocol, freeing it from any single implementation.
Avoid These BAD Practices in Python OOP
ArjanCodes provides a fantastic, practical example of why you should 'rely on abstractions'. This segment shows how to use a Protocol to define an abstraction and decouple a function from a concrete class.
Watch from 07:48 to 11:24. Pay close attention to the following steps: The initial 'bad' code where process_order directly creates an SMTPEmailService instance. The first improvement: injecting the dependency (dependency inversion). The final, most flexible solution: defining an EmailService protocol and using it as the type hint for the function argument. This allows any object that 'quacks like an email service' to be used.
This pattern is fundamental to writing clean, maintainable systems. By programming to an interface (the Protocol) rather than an implementation, you can easily swap out the SMTPEmailService for a SendGridEmailService or a MockEmailService for testing, without changing the process_order function at all.
Protocol vs. Abstract Base Classes (ABC)
You might be wondering how Protocol differs from Python's abc.ABC. Both are used to define interfaces, but they operate on the principles we discussed earlier:
abc.ABC: Enforces nominal subtyping. A class must explicitly inherit from the ABC to be considered a subtype. If you don't implement all abstract methods, Python raises aTypeErrorat instantiation time.typing.Protocol: Enforces structural subtyping. A class conforms if it has the right structure. This is checked by a static type checker likemypy, not at runtime (by default).
Watch this video for a direct comparison of the two.
Protocols vs ABCs in Python - When to Use Which One?
Let's watch another video from ArjanCodes that directly compares Protocols and ABCs and discusses when to use each.
Watch the segments from 01:04 to 01:51 for the core distinction and 13:53 to 14:57 for the discussion on use cases. The key takeaway is: use ABCs when you control the hierarchy and want a clear, explicit inheritance relationship; use Protocols when you need to define an interface for classes you don't control or want to avoid the rigidity of inheritance.
In summary:
| Feature | abc.ABC (Nominal) |
typing.Protocol (Structural) |
|---|---|---|
| Relationship | Explicit inheritance (class MyImpl(MyABC):) |
Implicit conformance (matching structure) |
| Enforcement | Runtime (TypeError on instantiation) |
Static time (mypy error) |
| Flexibility | Less flexible; requires modifying classes | More flexible; works with third-party code |
| Best Use Case | Defining a framework where you control all implementing classes. | Defining an interface for existing code or for maximum decoupling. |
Runtime Checking: The @runtime_checkable Decorator
By default, isinstance() checks fail with a protocol because they are designed for static analysis.
from typing import Protocol
class Greeter(Protocol):
def greet(self) -> str: ...
class Person:
def greet(self) -> str:
return "Hello"
p = Person()
# isinstance(p, Greeter) # -> TypeError: Instance and class checks can only be used with @runtime_checkable protocols
To enable runtime isinstance() checks, you can use the @runtime_checkable decorator from typing.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Greeter(Protocol):
def greet(self) -> str: ...
class Person:
def greet(self) -> str:
return "Hello"
p = Person()
print(isinstance(p, Greeter)) # -> True
However, be aware of two points:
- Performance: Runtime checking adds overhead, as Python must inspect the object's attributes and methods.
- Incompleteness: It only checks for the presence of the required methods/attributes, not their function signatures.
The ArjanCodes video you watched earlier (LINK) demonstrates this limitation clearly between 11:18 and 12:27. An object can pass an isinstance check but still cause a TypeError at runtime if its method signature doesn't match what's expected. For this reason, rely primarily on static checking with mypy for correctness and use @runtime_checkable sparingly when runtime introspection is truly necessary.
Conclusion
In this lesson, you've unlocked one of the most powerful and Pythonic features of the typing system. You now understand how typing.Protocol enables "static duck typing," providing the safety of interfaces without the rigidity of inheritance. For someone coming from a Java background, this is a crucial mental shift from nominal to structural typing.
Key Takeaways:
- Structural vs. Nominal Typing:
Protocolis for structural typing (what an object does), whileABCs and Javainterfaces are for nominal typing (what an object is). - Implicit Conformance: Classes do not need to inherit from a
Protocolto conform to it. They only need to have the correct structure (methods and attributes). - Decoupling: Protocols are a primary tool for decoupling application logic from concrete implementations, leading to more flexible and testable code.
- Static Enforcement: The main benefit of
Protocolis realized through static type checkers likemypy. @runtime_checkable: Use this decorator only when you explicitly need runtimeisinstance()checks, and be aware of its limitations.
In our next lesson, we will look at another specialized typing tool: TypedDict. You'll learn how to use it to precisely define the structure of dictionaries with fixed string keys, bringing type safety to a data structure that is often a source of runtime errors.