Python Protocol vs ABC: Choosing the Right Abstraction
python protocol vs abc: Compare Python's typing.Protocol and abc.ABC to decide when structural or nominal typing fits your design.
When you need to define a common interface in Python, you have two standard tools: abc.ABC and typing.Protocol. The choice between python protocol vs abc shapes how your code enforces contracts, how it behaves with isinstance, and how much flexibility you give to callers. Both let you declare methods that implementing classes must provide, but they differ fundamentally in how they verify conformance.
What ABC Provides: Nominal Subtyping
abc.ABC is the classic way to create abstract base classes. You define a class that inherits from ABC, mark methods with @abstractmethod, and any subclass must implement those methods before it can be instantiated.
from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self) -> float: pass class Circle(Shape): def __init__(self, radius: float): self.radius = radius def area(self) -> float: return 3.14159 * self.radius ** 2
Here Circle is a subtype of Shape because it inherits from it. This is nominal subtyping: a class is considered a Shape only if it explicitly declares that relationship. isinstance(circle, Shape) returns True because of the inheritance chain.
ABCs are useful when you control the class hierarchy and want to enforce a contract at class definition time. If a subclass forgets to implement an abstract method, Python raises a TypeError at instantiation, not later when the method is called.
What Protocol Provides: Structural Subtyping
typing.Protocol (PEP 544) enables structural subtyping. A class conforms to a Protocol if it has the required methods and attributes, regardless of whether it inherits from the protocol. You define a protocol by subclassing Protocol and declaring method signatures.
from typing import Protocol class AreaCalculable(Protocol): def area(self) -> float: ... class Square: def __init__(self, side: float): self.side = side def area(self) -> float: return self.side ** 2
Square does not inherit from AreaCalculable, but it is structurally compatible. When you annotate a function parameter with AreaCalculable, type checkers like mypy will accept Square because it has the matching area method.
Protocols are useful when you want to accept any object that provides the required behavior, even if the object comes from a third-party library or was not designed with your interface in mind.
Key Differences Between Protocol and ABC
| Aspect | abc.ABC | typing.Protocol |
|---|---|---|
| Subtyping model | Nominal (inheritance-based) | Structural (method-based) |
| Required inheritance | Yes, must subclass ABC | No, structural match is enough |
Runtime isinstance | Works by default | Requires @runtime_checkable |
| Method signature checking | Not enforced at runtime | Not enforced at runtime |
| Type checker support | Works with nominal typing | Works with structural typing |
| Use case | When you own the class hierarchy | When you want duck typing with type safety |
The most important distinction is how conformance is determined. ABCs rely on explicit inheritance, while Protocols rely on the shape of the object.
When to Use ABC Over Protocol
Choose abc.ABC when you are designing a framework or a library where you expect users to subclass your base class. For example, a plugin system where each plugin must implement a specific set of methods and you want to guarantee that at instantiation time.
class Plugin(ABC): @abstractmethod def run(self, context: dict) -> None: pass class LoggingPlugin(Plugin): def run(self, context: dict) -> None: print(context)
Because LoggingPlugin inherits from Plugin, isinstance(plugin, Plugin) is reliable and cheap. You can also use __subclasses__ to discover all plugin implementations, which is not possible with Protocols.
ABCs also allow you to define abstract properties, class methods, and static methods with the same @abstractmethod decorator. Protocols support those too, but the enforcement model is different.
When to Use Protocol Over ABC
Use typing.Protocol when you want to write functions that accept any object with a certain set of methods, without forcing callers to inherit from a specific class. This is especially valuable when integrating with external code that you cannot modify.
def total_area(shapes: list[AreaCalculable]) -> float: return sum(shape.area() for shape in shapes)
This function works with Circle, Square, or any other class that defines area(). If you used an ABC instead, you would need every shape class to inherit from Shape, which may not be possible if the class already has a different base class.
Protocols are also useful for defining interfaces that are only used at type-check time. You can define a protocol and use it purely as a type hint without any runtime cost, unless you add @runtime_checkable.
Runtime Considerations: isinstance and Performance
By default, isinstance does not work with a Protocol unless you decorate it with @runtime_checkable. Even then, the check is shallow: it only verifies that the object has the required methods and attributes, not that their signatures match.
from typing import Protocol, runtime_checkable @runtime_checkable class Drawable(Protocol): def draw(self) -> None: ... class Pen: def draw(self, color: str) -> None: pass print(isinstance(Pen(), Drawable)) # True, even though signature differs
This can lead to surprising behavior. The runtime check passes even if the method takes different arguments, because Python's isinstance only checks attribute presence. In contrast, an ABC's isinstance check is based on the class hierarchy and is guaranteed to reflect the actual inheritance.
Performance-wise, isinstance with an ABC is a simple lookup in the MRO. With a runtime_checkable Protocol, Python must inspect the object's attributes, which is slower. If you call isinstance frequently in a hot loop, an ABC is more efficient. However, for most applications the difference is negligible.
Common Pitfalls and How to Avoid Them
One common mistake is forgetting that a Protocol without @runtime_checkable cannot be used with isinstance. If you rely on runtime checks, you must add the decorator explicitly.
Another pitfall is assuming that a Protocol enforces method signatures. It does not, at runtime. Only static type checkers can catch signature mismatches, and they only do so when the code is analyzed.
ABCs have their own pitfalls. Forcing inheritance can create rigid hierarchies that are hard to change. If a class already inherits from another base class, adding an ABC may require multiple inheritance, which can lead to method resolution order conflicts.
Combining Protocol and ABC
You can combine both approaches. A class can inherit from an ABC to satisfy a nominal contract and also conform to a Protocol for structural typing. This is common when you want to provide a default implementation in the ABC while allowing external classes to be used structurally.
class Shape(ABC): @abstractmethod def area(self) -> float: ... class Circle(Shape): def area(self) -> float: return 3.14159 * self.radius ** 2 class AreaCalculable(Protocol): def area(self) -> float: ... def print_area(shape: AreaCalculable) -> None: print(shape.area()) print_area(Circle(2)) # Works because Circle has area()
Here Circle is both a Shape (nominal) and an AreaCalculable (structural). This gives you flexibility: you can use the ABC for runtime isinstance checks and the Protocol for type annotations that accept any compatible object.
The decision between python protocol vs abc ultimately depends on whether you need to enforce a contract through inheritance or through structure. When you own the class hierarchy and want guaranteed behavior at instantiation, ABC is the right tool. When you want to accept any object that has the required methods, especially from code you do not control, Protocol is the better choice.