PYTHON / OBJECT-ORIENTED PYTHON
Abstract base classes and protocols
Define enforced contracts with abc.ABC and abstractmethod, and structural contracts with typing.Protocol, and pick the right one per situation.
What you will learn
- Mark required methods with @abstractmethod on a class deriving from ABC
- Read __abstractmethods__ to see why instantiation raised TypeError
- Write a Protocol so unrelated classes match by shape, without inheriting
- Use @runtime_checkable when isinstance must work, knowing it only checks names
Understanding Abstract base classes and protocols
When a class derives from abc.ABC its metaclass, ABCMeta, scans the namespace for any attribute whose __isabstractmethod__ flag is True and stores those names in the class attribute __abstractmethods__. object.__new__ refuses to build an instance while that frozenset is non-empty, which is why a missing method surfaces as a TypeError at construction time rather than as an AttributeError much later at the first call. The @abstractmethod decorator itself does nothing but set that flag; all the enforcement lives in the metaclass, and an abstract method may still contain a real body that subclasses invoke through super().
typing.Protocol solves the opposite problem: the class is written to describe a shape, not to be inherited. If a protocol declares close(self) -> None, then any class with a compatible close method satisfies it as far as a static type checker is concerned, even a class from a library you cannot edit and that has never heard of your protocol. This is structural typing, and it keeps modules from having to share a common base class just to be used together.
The practical split is who checks and when. An ABC checks at runtime, on every instantiation, and lets you ship shared concrete methods alongside the abstract ones, so it fits a family of classes you own. A Protocol is checked by mypy or pyright and is invisible at runtime unless you add @runtime_checkable, and even then isinstance only verifies that attributes with those names exist, not that their signatures or return types match.
from abc import ABC, abstractmethod
class Ledger(ABC):
abstractmethod
def record(self, amount):
...
abstractmethod
def total(self):
...
def record_all(self, amounts): # concrete, built on the abstract parts
for amount in amounts:
self.record(amount)
class ListLedger(Ledger):
def __init__(self):
self._entries = []
def record(self, amount):
self._entries.append(amount)
def total(self):
return sum(self._entries)
class PartialLedger(Ledger):
def record(self, amount):
pass
books = ListLedger()
books.record_all([10.0, 2.5, 7.25])
print(books.total())
print(isinstance(books, Ledger))
try:
PartialLedger()
except TypeError as err:
print(type(err).__name__)
print(sorted(PartialLedger.__abstractmethods__))
An ABC is a contract subclasses sign and Python verifies when you instantiate; a Protocol is a shape classes satisfy by accident and a type checker verifies.
Worked examples
A protocol matched without inheritance
Shows a class satisfying a protocol by having the right method, and what @runtime_checkable does and does not check.
from typing import Protocol, runtime_checkable
runtime_checkable
class Closeable(Protocol):
def close(self) -> None:
...
class Socket: # never mentions Closeable
def close(self) -> None:
print("socket closed")
class Timer:
def stop(self) -> None:
print("timer stopped")
def shut_down(resource: Closeable) -> None:
resource.close()
shut_down(Socket())
print(Socket.__bases__)
print(issubclass(Socket, Closeable))
print(isinstance(Timer(), Closeable))
Example explained
Line 1shut_down annotates its parameter with Closeable, but the annotation is not consulted at runtime; the call works because Socket has close.
Line 2Socket.__bases__ is just (object,), proving the match is structural rather than through inheritance.
Line 3issubclass(Socket, Closeable) is True only because @runtime_checkable installed a hook that looks for the method name close.
Line 4Timer has stop instead of close, so the attribute lookup fails and isinstance reports False.
Abstract property and a registered virtual subclass
Combines @property with @abstractmethod and shows that ABC.register makes isinstance pass without checking anything.
from abc import ABC, abstractmethod
class Shape(ABC):
property
abstractmethod
def area(self):
...
class Square(Shape):
def __init__(self, side):
self.side = side
property
def area(self):
return self.side ** 2
class Blob: # no area at all
pass
Shape.register(Blob)
print(Square(3).area)
print(isinstance(Blob(), Shape))
print(hasattr(Blob(), "area"))
Example explained
Line 1@abstractmethod must be the innermost decorator so that @property wraps an already-flagged function.
Line 2Square overrides area with a concrete property, which clears the name from __abstractmethods__ and allows instantiation.
Line 3Shape.register(Blob) records Blob as a virtual subclass, so isinstance answers True.
Line 4hasattr is False, showing register performs no implementation check at all; the type claim is a promise, not a verification.
Important notes
issubclass() against a protocol works only when every member is a method; a protocol with data attributes raises TypeError even if it is runtime_checkable.
Abstract methods are not required to be empty. Giving one a useful body and calling super() from the override is a normal pattern, and it does not weaken the instantiation check.
Common mistakes
Using @abstractmethod on a class that does not inherit from ABC or set metaclass=ABCMeta: the decorator only sets a flag, nobody checks it, and the incomplete class instantiates happily until a missing method is called.
Calling isinstance(obj, SomeProtocol) on a protocol without @runtime_checkable, which raises TypeError about non-runtime protocols instead of returning a bool.
Trusting a runtime_checkable isinstance check to validate signatures: a class whose close(self, timeout) needs an extra argument passes the check and then raises TypeError at the actual call.
Try it yourself
Change, predict, then run
Write a runtime_checkable Protocol named SupportsArea with an area() method and a function total_area(items) that sums area() over an iterable, then add a plain class that fits it and one that does not and print isinstance for both.
Open the Python workspaceCheck your understanding
A class decorates two methods with @abstractmethod but inherits only from object. What happens when you instantiate it?
- TypeError, because @abstractmethod blocks instantiation on any class
- An ordinary instance is created; the decorator sets a flag that nothing checks without ABCMeta
- NotImplementedError is raised as soon as the object is constructed
- A DeprecationWarning is emitted and the abstract methods are removed from the class
Show answer
The refusal to instantiate comes from ABCMeta populating __abstractmethods__ and object.__new__ rejecting a non-empty set, so without ABC or metaclass=ABCMeta the flag is inert and construction succeeds. NotImplementedError is tempting because abstract bodies are often written to raise it, but that only happens if you call the method and its body raises, not at construction.