Back to Blog
Python

Python Protocol vs Abstract Base Class

Compare Python's Protocol and Abstract Base Class for defining interfaces. See how ABCs enforce contracts at runtime, how Protocols use structural typing, and when to choose each.

typingabstract-base-classprotocolstructural-typinginterfaces
Illustration comparing Python Protocol and Abstract Base Class interface definitions

When you need to define an interface in Python, two standard tools come to mind: typing.Protocol and abc.ABC. Both let you declare methods that concrete classes must implement, but they enforce that contract in fundamentally different ways. Understanding how they differ affects runtime behavior, type checking, and how flexible your code remains.

What Both Approaches Solve

Both Protocol and ABC exist to formalize the shape of an object. They answer the question: "What methods and attributes must a class expose to be used here?" Without such a contract, you rely on duck typing and hope that callers pass objects with the right methods. With an explicit interface, you get early feedback when an implementation is incomplete.

The key difference lies in how the contract is verified. An abstract base class uses inheritance: a concrete class must subclass the ABC and implement its abstract methods. A protocol uses structural subtyping: any class that has the required members is considered compatible, regardless of its inheritance hierarchy.

How ABCs Enforce Structure at Runtime

An ABC is defined by subclassing abc.ABC and decorating methods with @abstractmethod. Any concrete subclass must override all abstract methods, or Python raises a TypeError at instantiation time.

from abc import ABC, abstractmethod class Repository(ABC): @abstractmethod def get(self, key: str) -> str: ... @abstractmethod def save(self, key: str, value: str) -> None: ... class InMemoryRepository(Repository): def get(self, key: str) -> str: return "value" def save(self, key: str, value: str) -> None: pass

If you forget to implement save, InMemoryRepository() raises TypeError: Can't instantiate abstract class InMemoryRepository with abstract method save. This check happens at runtime, during instantiation, not at import time.

Because the ABC relies on inheritance in normal use, making an existing class conform usually means modifying its class hierarchy. ABC.register() can create a virtual subclass without editing the class, but only if you register every class manually; it does not add missing methods. This is a deliberate tradeoff: the inheritance path gives strong runtime guarantees, but you lose flexibility when working with third-party classes or when you want to avoid inheritance for other reasons.

How Protocols Enable Structural Subtyping

A Protocol is a class that inherits from typing.Protocol. It defines method signatures but does not require inheritance. Instead, type checkers like mypy or pyright verify that an object has the required attributes and methods. At runtime, a protocol is just a regular class unless you decorate it with @runtime_checkable.

from typing import Protocol, runtime_checkable @runtime_checkable class Repository(Protocol): def get(self, key: str) -> str: ... def save(self, key: str, value: str) -> None: ... class InMemoryRepository: def get(self, key: str) -> str: return "value" def save(self, key: str, value: str) -> None: pass

With @runtime_checkable, you can use isinstance(obj, Repository) and Python will check that obj has the required methods. Without it, isinstance raises TypeError because protocols are not meant for runtime checks unless explicitly enabled.

The important nuance is that @runtime_checkable only checks for the presence of methods and attributes, not their signatures. So isinstance may return True even if the method has a different parameter list. Static type checkers, however, do verify signatures precisely.

Comparing Runtime Behavior and Type Checking

AspectABCProtocol
Inheritance requiredNormally yesNo
Runtime enforcementAt instantiationOnly with @runtime_checkable
Signature checking at runtimeNoNo (only presence)
Static type checkingYes, via subclassingYes, via structural matching
Works with third-party classesOnly via manual register()Yes
Multiple inheritancePossible but can complicate method resolution orderUsually not needed

ABCs give you a runtime guarantee tied to a specific class hierarchy, either through a concrete subclass or through explicit virtual subclass registration. Protocols give you a softer guarantee based on the shape of the object. This difference is central to the decision between the two.

When to Choose an ABC

Use an ABC when you want direct runtime enforcement. If you are building a framework where users must implement a specific set of methods, and you want to fail fast with a clear error, an ABC is often the safer choice. ABCs also allow you to provide default implementations for some methods while leaving others abstract.

Another reason to choose an ABC is when you want to share implementation code. An ABC can contain concrete methods that subclasses inherit, reducing duplication. A Protocol is primarily an interface declaration; if you also need shared implementation, an ABC or separate mixin is more explicit and less surprising.

When to Choose a Protocol

Reach for a Protocol when you want to accept any object that behaves correctly, even if it does not inherit from a common base. This is especially useful when integrating with third-party libraries where you cannot modify class hierarchies. Protocols also work well with duck typing and functional programming patterns where you prefer composition over inheritance.

If you rely heavily on static type checking and want to catch interface mismatches at development time, a Protocol gives you precise signature validation without forcing a specific inheritance tree. This makes your code more flexible and easier to test with lightweight mock objects.

Performance and Overhead Considerations

The runtime cost of ABCs comes from the abc.ABCMeta metaclass and the checks performed during instantiation. This overhead is negligible for most applications, but it exists. Protocols with @runtime_checkable add a small cost when isinstance is called, because Python must inspect the target object's attributes. Without @runtime_checkable, a Protocol adds no per-call runtime overhead; it is purely a typing construct.

Neither approach should be a performance bottleneck in typical code. The more important consideration is the cost of a wrong interface: an ABC will raise an error early, while a Protocol may fail later if you rely on duck typing without static analysis. That tradeoff often matters more than microsecond-level runtime differences.

Common Pitfalls and Compatibility Notes

A frequent mistake with @runtime_checkable is expecting it to validate method signatures. It does not. If you need signature enforcement, you must rely on a static type checker. Another pitfall is using a Protocol as a base class for concrete implementations. While possible, it can lead to confusion because a Protocol is meant for interface declaration, not for providing shared behavior.

ABCs can also cause issues when you use multiple inheritance. If two ABCs define the same abstract method, you must carefully resolve the method resolution order (MRO). Protocols avoid this problem when you use them structurally, because compatibility does not depend on the inheritance hierarchy.

A Practical Example: Repository Pattern

To see the difference in action, consider a repository interface used by a service layer. With an ABC, the service is usually designed to receive a concrete subclass of Repository. With a Protocol, the service accepts any object with get and save methods.

# ABC version class UserService: def __init__(self, repo: Repository): self.repo = repo # Protocol version class UserService: def __init__(self, repo: Repository): # same type hint, but now structural self.repo = repo

The type hint is identical, but the runtime behavior differs. With an ABC, the structural type hint alone does not create a runtime check when you call UserService(repo). The runtime guarantee comes from the concrete subclass: Python prevents instantiating an ABC subclass until all abstract methods are implemented. With a Protocol, there is no runtime check unless you explicitly call isinstance(repo, Repository). In both cases, a static type checker can flag a mismatched argument at development time.

The choice between them ultimately depends on whether you need runtime guarantees or structural flexibility. For library code that must protect against misuse, an ABC is often better. For application code that wants to stay open to different implementations, a Protocol is more idiomatic.

Python Protocol vs Abstract Base Class: Key Differences and Use Cases | RYUSLOG DEV