Python has two ways to express the idea "this class implements an interface", and they look superficially similar but enforce the contract at completely different moments. Abstract Base Classes (abc.ABC + @abstractmethod) have been around since Python 2.6 (PEP 3119, 2007); they use nominal subtyping, where a class IS an implementer only if it inherits from the ABC, and enforce the contract at instantiation time by raising TypeError. Protocols (typing.Protocol) arrived in Python 3.8 (PEP 544, 2017); they use structural subtyping, where any class with the right methods satisfies the Protocol regardless of inheritance, and enforcement happens at static type-check time. The choice between them isn't a question of which is better; it's a question of WHEN you want the contract violation to surface and WHO you expect to implement the interface. This entry explains both mechanics and gives clear decision rules. One of 10 explainers in our Python OOP complete guide.
Key Takeaways
ABC = nominal subtyping. Class must explicitly inherit; enforcement at instantiation via TypeError.
Protocol = structural subtyping. Any class with matching methods satisfies; enforcement at static type-check time.
Use ABC when you own the implementations, need runtime guarantees, or want to share default helper code.
Use Protocol when accepting third-party types, writing libraries, or only needing static-time safety.
@runtime_checkable lets Protocols work with isinstance(), but only checks method NAMES, not signatures.
ABC inherits explicitly and fails loudly at instantiation, while Protocol matches structurally and fails earlier at static-check time — same contract enforced from completely different moments in the lifecycle.
Why Interfaces at All?
Python's duck typing makes a class usable as long as it has the right methods at call time. So why bother with formal interfaces? Three reasons:
Clarity: the interface document the contract explicitly, not as a comment but as code the reader can lint against.
Early failure: a missing method surfaces at construction or static-check time, not three call frames deep when something tries to use it.
Type-checker support: tools like mypy and pyright can validate that the right type flows through your function signatures.
For trivial code, duck typing is fine. For libraries, large applications, or anywhere two teams share types across a boundary, formal interfaces pay back the small amount of extra code many times over.
ABC: The Original Approach
abc.ABC is a base class that uses the ABCMeta metaclass. Inherit from it, decorate methods with @abstractmethod, and Python tracks which methods MUST be implemented in concrete subclasses.
from abc import ABC, abstractmethod
class Storage(ABC):
@abstractmethod
def read(self, key: str) -> bytes: ...
@abstractmethod
def write(self, key: str, data: bytes) -> None: ...
class S3Storage(Storage):
def read(self, key):
return b"..."
def write(self, key, data):
pass
s = S3Storage() # Works — both methods implemented
If you forget one, instantiation fails loudly:
class BrokenStorage(Storage):
def read(self, key):
return b"..."
# Forgot to implement write()
s = BrokenStorage()
# TypeError: Can't instantiate abstract class BrokenStorage
# with abstract methods write
The error message names the missing methods. You learn about the broken contract at the line where you construct the instance, not later when something calls write() and gets AttributeError.
How ABCMeta Tracks Abstract Methods
The mechanism: every class created with ABCMeta as its metaclass gets an __abstractmethods__ attribute, a frozenset of method names still marked abstract.
Instantiation checks cls.__abstractmethods__; if it's not empty, the metaclass blocks construction. You can inspect this directly when debugging.
Abstract Methods Can Have Default Implementations
@abstractmethod doesn't mean "no body". The decorated method can have a usable implementation that subclasses CAN call via super():
class Storage(ABC):
@abstractmethod
def read(self, key):
# Default behavior subclasses can call via super().read(key)
raise NotImplementedError(f"{type(self).__name__} must implement read")
class S3Storage(Storage):
def read(self, key):
super().read(key) # Triggers the abstract default
# would only run if we wanted the error
return b"..."
This pattern is unusual but legal. More commonly, the abstract method's body is just pass or ....
Virtual Subclasses with ABC.register()
ABCs let you declare that an existing class (one you don't control, can't modify, doesn't even inherit from the ABC) satisfies the interface. This is the virtual subclass mechanism.
The register() call doesn't add methods or change behavior; it just tells isinstance and issubclass to return True for that pair. Use it sparingly; it bypasses the abstractmethod check, so you're trusting yourself that the registered class actually implements what it claims.
Protocol: The Structural Approach
typing.Protocol (PEP 544, Python 3.8+) takes the opposite stance: any class with the right methods satisfies the Protocol, with no inheritance required. The implementer doesn't even need to know the Protocol exists.
The type checker (mypy, pyright) validates the structural match at static analysis. At runtime, Python doesn't enforce anything. If you pass an object missing a method, the error fires only when that method is called.
@runtime_checkable for isinstance Support
By default, isinstance(obj, MyProtocol) raises TypeError. Add the @runtime_checkable decorator to enable runtime checks, but with a caveat:
The caveat: @runtime_checkable verifies only that the right method NAMES exist on the object. It does NOT check signatures, return types, or anything else. The PSF docs flag this explicitly: a class with an unrelated method named read taking three arguments still passes isinstance. Use static checks when you can; use @runtime_checkable for guards where loose matching is acceptable.
ABC vs Protocol Decision Matrix
Question
Choose ABC
Choose Protocol
Do you control all implementations?
Yes
No / third-party
Need RUNTIME enforcement of contract?
Yes
Static-only is fine
Want to share default helper methods?
Yes
No (Protocols can't share code)
Library author exposing a hook point?
Sometimes
Preferred
Need isinstance() checks?
Built-in
Need @runtime_checkable
Implementing class shouldn't depend on you?
No (inheritance forces it)
Yes (decoupled)
Working with framework that expects ABC?
Yes
No
Real Differences in Practice
Coupling
ABC: S3Storage(Storage) means S3Storage permanently knows about Storage, so if you reorganize and rename Storage, every S3Storage-like class must update in lockstep.
Protocol: S3Storage has no idea Storage exists, which means you can introduce, rename, or split the Protocol on the consumer side without touching any implementer in the codebase.
Enforcement Timing
ABC catches the missing method at the FIRST instantiation: runtime, but early enough in execution that the broken construction site is itself the failure point.
Protocol catches the mismatch at static type-check, BEFORE execution begins, so if you don't run mypy or pyright as part of CI, Protocol catches nothing until the missing method is finally called.
Code Sharing
ABC can carry default implementations of non-abstract methods, and concrete subclasses inherit those helpers for free along with the abstractmethod hooks.
Protocol cannot share code at all because it's a structural declaration rather than a real class, so implementers must provide every method body themselves without any inherited scaffolding.
Multiple Interfaces
ABC: multiple inheritance from multiple ABCs works but invokes the MRO machinery covered in the super + MRO guide, with all the cooperative inheritance complications that come with it.
Protocol: a class can satisfy any number of Protocols without inheriting from any of them, so there's no MRO ordering, no diamond risk, and no metaclass conflicts to worry about.
When to Use Both Together
ABCs and Protocols aren't mutually exclusive. A common pattern in larger projects:
ABCs internally for code that the library owns, where you want to share default behavior across implementations. collections.abc.MutableMapping is the canonical example: inherit from it, implement a few core methods, and get many more for free.
Protocols externally on the public API surface, so library users can plug in their own types without inheriting from anything you ship.
This way the framework code gets the runtime guarantees and shared helpers, and the user code stays decoupled from your inheritance hierarchy.
Real-World Example: A Plugin Loader
A loader that accepts plugins implementing a known interface. Two versions:
If someone writes a Plugin subclass that forgets run, instantiation fails immediately.
Plugin Loader with Protocol
from typing import Protocol
class Plugin(Protocol):
def name(self) -> str: ...
def run(self, payload: dict) -> dict: ...
class HelloPlugin: # No inheritance!
def name(self): return "hello"
def run(self, payload): return {"echo": payload}
def load(plugin_cls: type[Plugin]) -> Plugin:
return plugin_cls()
p = load(HelloPlugin)
HelloPlugin doesn't import or know about Plugin. A third party could write their own plugin without depending on your codebase. The type checker catches mismatches; nothing runtime-blocks a broken plugin from being loaded.
Common Mistakes
1. Using ABC just for the abstract method check
If you don't need shared helper methods or virtual subclasses, Protocol is usually a better fit. ABC's inheritance coupling is overhead you're not using.
2. Forgetting @runtime_checkable when you need isinstance()
Plain Protocols can't be used with isinstance(); you'll get TypeError. Add @runtime_checkable if you need runtime checks, and remember its limitations (NAMES only, not signatures).
3. Using @abstractmethod outside of an ABC subclass
The decorator is silent if the containing class doesn't use ABCMeta. No error fires; the method just acts like a normal method. Always inherit from abc.ABC (or set metaclass=ABCMeta) when using @abstractmethod.
4. Putting state in a Protocol
Protocols can declare attributes, but they're structural declarations, not initialization. Don't try to use a Protocol as a partial constructor. If you need shared state, use an ABC with a real __init__.
5. Over-using ABC.register()
Virtual subclasses skip the abstractmethod enforcement. You're promising on behalf of a class that you might not control. Reserve it for cases where you genuinely cannot modify the third-party class. Otherwise, just refactor properly.
Frequently Asked Questions
What is the difference between abc.ABC and typing.Protocol in Python?
abc.ABC uses nominal subtyping: a class must explicitly inherit from your ABC to be considered an implementer. typing.Protocol uses structural subtyping (duck typing): any class with the right methods and signatures satisfies the Protocol, no inheritance required. ABC enforcement happens at runtime when you try to instantiate a class that didn't implement all @abstractmethod-decorated methods, at which point Python raises TypeError. Protocol enforcement happens at static type-check time: mypy or pyright flags the mismatch before the code runs. ABC blocks bad code from running; Protocol catches bad code before you ship it. Both are valid; the choice depends on whether you control implementations and whether you need runtime guarantees.
When should I use ABC instead of Protocol in Python?
Choose ABC when (1) you control all implementations and want runtime guarantees that all methods are present, (2) you want to share default implementations or helper methods alongside the contract, (3) you need an explicit registration mechanism for virtual subclasses via ABC.register(), or (4) you're working within a framework that already uses ABC patterns (Django's contributing classes, collections.abc). Choose Protocol when you're accepting third-party types you don't control, you're a library author wanting users to plug in their own types without inheriting from your base, or you only need static type-check enforcement. The runtime cost of ABCs and the inheritance coupling they impose are the main reasons to prefer Protocol when those guarantees aren't required.
What happens if I forget to implement an abstractmethod in Python?
When you decorate a method with @abstractmethod inside an ABC subclass, Python registers it in the class's __abstractmethods__ set. When you try to instantiate the subclass, the ABCMeta metaclass checks whether all abstractmethods have concrete implementations in the MRO. If any are missing, it raises "TypeError: Can't instantiate abstract class X with abstract methods Y, Z". The error names which methods are unimplemented, making the fix obvious. This is the core value of ABCs versus duck typing: the mismatch surfaces at the construction site, with a clear message, instead of producing AttributeError much later when the missing method is called. Defining the class itself is fine; the error only fires when you try to make an instance.
What does @runtime_checkable do for typing.Protocol?
By default Protocols are STATIC-ONLY: type checkers verify the structural match, but isinstance(obj, MyProtocol) raises TypeError because the Protocol can't introspect at runtime. The @runtime_checkable decorator (added in Python 3.8 along with Protocol itself) enables isinstance and issubclass to work by checking the presence of required attribute names. It's looser than ABC enforcement, since it only checks names exist (not their signatures), but it lets you write isinstance() guards when needed. The PSF docs note this limitation explicitly: runtime_checkable matches names only, so a class with an unrelated method named the same as your Protocol's method still passes. Use it carefully, and prefer static checks when possible.
Can I use ABC and Protocol together in the same Python project?
Yes. They solve different problems and frequently coexist. A common pattern: ABCs internally for code that the library owns and wants to share default implementations across (an ABC with concrete helper methods plus @abstractmethod hooks for subclasses to fill in). Protocols externally for the public API surface, so library users can plug in their own types without inheriting from anything. Python's standard library does this: collections.abc defines ABCs like Iterable, Container, Hashable that you can inherit from to get default behavior, but the type-hint world also accepts Protocol-style structural matches via the same names through typing.Iterable. The choice per use case is independent; you don't need to commit to one paradigm project-wide.
The Bottom Line: Pick Based on Who Implements and When Errors Should Surface
ABCs and Protocols both express the idea "this class implements an interface", but they enforce the contract at different moments and impose different coupling. ABCs catch missing methods at instantiation, share default helper code through inheritance, and force a nominal "is-a" relationship. Protocols catch mismatches at static type-check time, impose no inheritance, and let third-party types satisfy your interface without knowing about it. Use ABCs when you own implementations and need runtime guarantees; use Protocols when you're a consumer of unknown types or a library author exposing a hook point. Both can coexist in one project. Next entry: @dataclass deep dive: from boilerplate to frozen and slots. Or browse the full Python OOP guide.
Practice Interfaces Until They're Reflex
CodeGym's Python track teaches ABCs and Protocols side-by-side across 62 levels with hands-on tasks: building plugin systems, defining interfaces for third-party code, and getting both runtime and static-check enforcement right. AI validators catch the easy traps (missing @runtime_checkable, accidental ABC.register over-use); AI mentors explain when to choose which. First level free; full plan on the pricing page.
GO TO FULL VERSION