Python Interface Pattern: ABCs and Protocols
Learn how to implement the python interface pattern using abstract base classes, protocols, and duck typing, with practical guidance for choosing the right approach.
When Python developers talk about the python interface pattern, they usually mean a way to define a contract that multiple classes can fulfill. Unlike languages with explicit interface keywords, Python offers several mechanisms: abstract base classes (ABCs), protocols, and plain duck typing. Each approach has different tradeoffs in terms of type checking, runtime behavior, and maintainability.
The Python Interface Pattern and Its Purpose
The interface pattern is about defining a common API that multiple implementations can follow. This enables polymorphism, dependency inversion, and easier testing. In Python, there is no single interface keyword; you choose among ABCs, protocols, or rely on duck typing. The choice affects how strictly the contract is enforced and how flexible your code remains as it evolves.
Defining Interfaces with Abstract Base Classes
Abstract base classes are the classic way to define an interface in Python. You create a class that inherits from ABC and use the @abstractmethod decorator to declare methods that subclasses must implement.
from abc import ABC, abstractmethod class Repository(ABC): @abstractmethod def get(self, key: str) -> str: pass @abstractmethod def save(self, key: str, value: str) -> None: pass class InMemoryRepository(Repository): def __init__(self): self._data = {} def get(self, key: str) -> str: return self._data[key] def save(self, key: str, value: str) -> None: self._data[key] = value
Here, Repository cannot be instantiated directly. Any subclass must implement get and save; otherwise, Python raises a TypeError at instantiation time. This provides a compile-time-like guarantee that the interface is honored, which is useful in large codebases where you want to enforce a contract across many classes.
Using Protocols for Structural Subtyping
Protocols, introduced in Python 3.8 via typing.Protocol, offer structural subtyping. A class is considered a subtype of a protocol if it has the required methods, regardless of inheritance. This is closer to Go's interface style and is ideal for duck typing with static type checking.
from typing import Protocol class Repository(Protocol): def get(self, key: str) -> str: ... def save(self, key: str, value: str) -> None: ... class InMemoryRepository: def __init__(self): self._data = {} def get(self, key: str) -> str: return self._data[key] def save(self, key: str, value: str) -> None: self._data[key] = value
Notice that InMemoryRepository does not inherit from Repository. It still satisfies the protocol because it implements the required methods. Static type checkers like mypy can verify that a function expecting a Repository can accept an InMemoryRepository. At runtime, protocols are inert unless you decorate them with @runtime_checkable, which enables isinstance checks but with limitations.
Duck Typing: The Implicit Interface
Duck typing is the most Pythonic form of interfaces: if an object has the methods you call, it works. You don't declare an interface at all.
def process_repository(repo): value = repo.get("key") repo.save("key", value.upper())
This function works with any object that has get and save methods. It is flexible and requires no boilerplate. The downside is that errors surface only at runtime when a method is missing, and static type checkers cannot verify the contract without additional hints.
Choosing Between ABC and Protocol
The decision between ABC and Protocol depends on whether you need runtime enforcement or prefer structural typing. The table below summarizes the key differences.
| Criterion | ABC | Protocol |
|---|---|---|
| Inheritance required | Yes | No |
| Runtime enforcement | Yes (instantiation check) | Only with @runtime_checkable |
| Static type checking | Yes | Yes |
| Best for | Internal class hierarchies | Public APIs, third-party classes |
| Coupling | Strong (forces inheritance) | Loose (structural) |
Use an ABC when you control the class hierarchy and want to guarantee that all subclasses implement the interface. Use a Protocol when you want to define a contract for existing classes without forcing them to inherit from your base class, especially in library code where users may already have their own base classes.
Common Pitfalls When Implementing Interfaces
A frequent mistake is using an ABC for a simple interface that only needs structural typing. This forces all implementers to inherit from your class, which can be problematic in multiple inheritance scenarios or when dealing with third-party code. Another pitfall is forgetting to call super().__init__() in an ABC's __init__ method; while not always required, it can break initialization chains. Also, overusing @runtime_checkable on protocols can lead to surprising behavior because it only checks method presence, not signatures.
Runtime and Maintainability Considerations
ABCs add a small runtime overhead for isinstance checks and method resolution, but this is rarely significant. Protocols, unless decorated with @runtime_checkable, have zero runtime cost because they exist only for type checkers. From a maintainability perspective, protocols encourage loose coupling: you can evolve the contract without forcing implementers to change inheritance. ABCs, on the other hand, provide a clear, explicit contract that is enforced at instantiation, which can be valuable in large teams where discipline is needed.
A practical approach is to combine both: use a Protocol as the public interface for your library, and internally use an ABC if you need to share implementation logic. For example, you might define a Repository protocol and then provide an AbstractRepository ABC that implements common helper methods while leaving the core methods abstract.
from typing import Protocol from abc import ABC, abstractmethod class Repository(Protocol): def get(self, key: str) -> str: ... def save(self, key: str, value: str) -> None: ... class AbstractRepository(ABC): def log_access(self, key: str) -> None: print(f"Accessing {key}") @abstractmethod def get(self, key: str) -> str: ... @abstractmethod def save(self, key: str, value: str) -> None: ...
This hybrid pattern gives you the flexibility of structural typing for consumers and the convenience of shared code for implementers. When deciding which interface pattern to use, consider how your codebase will evolve: if you expect many third-party implementations, protocols are safer; if you want to enforce a strict contract internally, ABCs are the way to go.