When to Use ABCs vs Protocols
Swipe to show menu
Both ABCs and Protocols define interfaces. Choosing between them is not about which is "better" – it is about what guarantees you need and how much control you have over the classes that will implement the interface.
Use ABCs When You Want Enforcement
ABCs raise TypeError at instantiation if abstract methods are missing. This makes them the right choice when you own the class hierarchy and want to guarantee that every concrete class is complete before it can be used:
1234567891011121314151617181920212223242526from abc import ABC, abstractmethod # ABC is the right choice – you control all implementations class PaymentGateway(ABC): @abstractmethod def charge(self, amount): pass @abstractmethod def refund(self, transaction_id): pass # Shared logic that all gateways use def process(self, amount, idempotency_key): print(f"Processing payment {idempotency_key}") return self.charge(amount) class StripeGateway(PaymentGateway): def charge(self, amount): return f"Stripe charged ${amount:.2f}" def refund(self, transaction_id): return f"Stripe refunded {transaction_id}" gateway = StripeGateway() print(gateway.process(99.99, "idem-001"))
ABCs are also the right choice when you need mixin methods – concrete behavior in the base class that all subclasses inherit automatically.
Use Protocols When You Want Flexibility
Protocols are the right choice when you cannot or do not want to require inheritance. This includes:
- Integrating third-party classes you cannot modify;
- Writing library code that accepts any object with the right methods;
- Describing an interface for static type checking without runtime coupling.
1234567891011121314151617181920from typing import Protocol, runtime_checkable # Protocol is the right choice – you do not control the implementations @runtime_checkable class Exportable(Protocol): def export(self, data: list) -> str: ... # Third-party class – cannot be modified class PandasDataFrame: def export(self, data): return f"DataFrame export: {len(data)} rows" # Built-in type – cannot inherit from anything class BuiltinList(list): def export(self, data): return str(data) print(isinstance(PandasDataFrame(), Exportable)) # True – has export() print(isinstance(BuiltinList(), Exportable)) # True – has export()
The Decision Framework
Ask these questions to choose the right tool:
- Do you own all the classes that will implement the interface? → ABC;
- Do you need shared behavior (mixin methods) in the base class? → ABC;
- Do you need
TypeErrorenforcement at instantiation? → ABC; - Are you integrating third-party or built-in classes? → Protocol;
- Are you writing a library that accepts "duck-typed" objects? → Protocol;
- Do you only need static type checking with no runtime overhead? → Protocol.
They Can Coexist
ABCs and Protocols are not mutually exclusive. A codebase can use ABCs for its own class hierarchies and Protocols for its public-facing APIs:
12345678910111213141516171819202122from abc import ABC, abstractmethod from typing import Protocol, runtime_checkable # Internal hierarchy uses ABC – enforcement guaranteed class InternalTransformer(ABC): @abstractmethod def transform(self, data): pass # Public API uses Protocol – accepts any compatible object @runtime_checkable class TransformerLike(Protocol): def transform(self, data) -> list: ... # The internal class satisfies the public Protocol automatically class FilterTransformer(InternalTransformer): def transform(self, data): return [item for item in data if item] transformer = FilterTransformer() print(isinstance(transformer, TransformerLike)) # True
Summary Table
Thanks for your feedback!
Ask AI
Ask AI
Ask anything or try one of the suggested questions to begin our chat