Two ways to define an interface in Python
Python is famous for duck typing — "if it walks like a duck and quacks like a duck, it's
a duck." But modern Python gives you two formal tools for defining interfaces on top of
that: Abstract Base Classes (ABCs) and typing.Protocol. They look similar at a
glance but rest on opposite philosophies — nominal subtyping vs structural
subtyping — and that difference drives every practical choice between them.
The core distinction
ABCs use nominal (explicit) subtyping: a class satisfies the interface only if it
explicitly declares class MyClass(MyABC). Python enforces this at runtime by
refusing to instantiate classes that miss abstract methods.
Protocols use structural (implicit) subtyping: a class satisfies the interface if it simply has the right methods and attributes — no inheritance declaration required. Type checkers like mypy enforce this statically, at lint/check time.
Quick-reference comparison
abc.ABC + @abstractmethod | typing.Protocol | |
|---|---|---|
| Subtyping style | Nominal — must class Foo(MyABC) | Structural — matching methods is enough |
| Explicit registration? | Yes (inherit or register()) | No — implicit via shape |
| Runtime enforcement? | Yes — TypeError on missing methods | Only with @runtime_checkable |
| Static type checking? | Yes | Yes (primary use case) |
isinstance() works? | Yes | Only with @runtime_checkable |
| Shared implementation? | Yes — concrete mixin methods | No — Protocols are pure interface |
| Third-party classes? | Via register() hack | Yes — if they have the right shape |
collections.abc integration? | Yes | Separate |
| Best for | Families of related classes you control | Defining callable/structural contracts, typing third-party code |
| Added in | Python 2.6+ (abc module) | Python 3.8+ (typing.Protocol) |
ABCs — explicit contracts with runtime enforcement
An ABC requires that every concrete subclass explicitly inherits from it and implements all abstract methods. The enforcement happens at instantiation time — not at definition time — which catches missing methods early.
from abc import ABC, abstractmethod
class Serializer(ABC):
@abstractmethod
def serialize(self, data: dict) -> str: ...
@abstractmethod
def deserialize(self, text: str) -> dict: ...
def round_trip(self, data: dict) -> dict: # concrete mixin method
return self.deserialize(self.serialize(data))
class JsonSerializer(Serializer):
def serialize(self, data):
import json; return json.dumps(data)
def deserialize(self, text):
import json; return json.loads(text)
Serializer() # TypeError — can't instantiate abstract class
JsonSerializer() # OK — all abstract methods implemented
# Works even without explicit subclassing if you use register():
class ThirdPartySerializer:
def serialize(self, data): ...
def deserialize(self, text): ...
Serializer.register(ThirdPartySerializer)
isinstance(ThirdPartySerializer(), Serializer) # True — registered
collections.abc provides battle-tested ABCs for standard container protocols:
Iterable, Sequence, Mapping, Callable, and many more. Subclassing these gives you
free mixin implementations (e.g. subclass Sequence with just __getitem__ and __len__
and get __contains__, __iter__, index, count for free).
Use ABCs when you have a tight family of classes that must share a contract and you control the class hierarchy — and when you want clear runtime errors at instantiation.
Deep dive: Abstract Base Classes & Protocols interview questions
Protocols — structural typing for duck typing with type safety
typing.Protocol lets you define an interface that any class satisfies simply by having
the right methods and attributes — no inheritance required. This is how duck typing
works at the type-checker level: a class "is a" Drawable if it has a draw() method,
full stop.
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
class Circle: # no inheritance from Drawable
def draw(self) -> None:
print("drawing circle")
class Square: # also has draw() — also satisfies Drawable
def draw(self) -> None:
print("drawing square")
def render(shape: Drawable) -> None: # type checker accepts any matching type
shape.draw()
render(Circle()) # OK — Circle has draw()
render(Square()) # OK — Square has draw()
render("hello") # mypy error — str has no draw()
Neither Circle nor Square inherits from Drawable. The type checker accepts them
purely because their shape matches. At runtime there's no check unless you add
@runtime_checkable:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Drawable(Protocol):
def draw(self) -> None: ...
isinstance(Circle(), Drawable) # True — runtime shape check
isinstance("hello", Drawable) # False
Note: @runtime_checkable only checks for method/attribute presence, not signatures.
Use Protocols when:
- You're writing a function that accepts any class with a given shape, including third-party classes you can't modify.
- You want duck-typed flexibility with static type safety.
- The relationship is a capability ("can be drawn") rather than a family ("is a Shape").
Deep dive: Generics & Protocols interview questions
The key practical difference — third-party classes
The most important real-world difference: you can't make a third-party class inherit
from your ABC (without register(), which is a workaround). But any third-party class
that happens to have the right methods automatically satisfies a Protocol.
import pandas as pd
# ABC: pandas DataFrame does NOT subclass your ABC — won't work without register()
class Tabular(ABC):
@abstractmethod
def to_csv(self) -> str: ...
isinstance(pd.DataFrame(), Tabular) # False — unless registered manually
# Protocol: DataFrame has to_csv() — it automatically satisfies the Protocol
from typing import Protocol
class Tabular(Protocol):
def to_csv(self) -> str: ...
def export(data: Tabular) -> None:
print(data.to_csv())
export(pd.DataFrame()) # type checker: OK (DataFrame has to_csv)
This is why modern Python typing favours Protocols for library code and public APIs.
When to share implementation — ABCs win
Protocols are pure interface — no concrete methods. If you need shared implementation alongside the contract, use an ABC:
class Animal(ABC):
@abstractmethod
def speak(self) -> str: ...
def describe(self) -> str: # concrete mixin — shared by all subclasses
return f"I am a {type(self).__name__} and I say {self.speak()}"
class Dog(Animal):
def speak(self) -> str: return "woof"
Dog().describe() # "I am a Dog and I say woof"
The decision rule
| Situation | Choose |
|---|---|
| Family of related classes you control, need runtime enforcement | ABC |
| Need shared (mixin) implementation alongside the contract | ABC |
| Want to type-hint a structural capability across unrelated types | Protocol |
| Need to accept third-party classes without modifying them | Protocol |
isinstance() checks at runtime are required | ABC (or @runtime_checkable Protocol) |
| Modern library/API code with type checking | Protocol |
Recap
ABCs use nominal subtyping — classes must explicitly inherit from the ABC and
implement every abstract method, which Python enforces at instantiation time. They support
shared mixin implementations and integrate cleanly with isinstance. Protocols use
structural subtyping — any class with the right methods and attributes satisfies the
protocol without declaring it, which type checkers (mypy, pyright) verify statically. Add
@runtime_checkable to get isinstance support, though only method presence is checked.
Default to a Protocol for library code and capability-based interfaces; reach for an
ABC when you need a tight class hierarchy, shared concrete methods, or reliable
runtime enforcement.