
Abstract Base Classes in Python with the abc Module
Python leans on duck typing: if an object has the method you need, you call it and move on. That works well until you have a family of classes that are all supposed to implement the same set of methods, and one of them quietly forgets. You find out at runtime, deep inside some code path, with an AttributeError or a NotImplementedError that a test never hit.
Abstract base classes (ABCs) move that failure to the moment you create the object. You declare which methods a subclass must provide, and Python refuses to instantiate any class that hasn't provided them. The abc module in the standard library gives you the tools, and collections.abc gives you a set of ready-made ABCs for containers.
I'll cover how to define an abstract class, abstract properties and class methods, giving abstract methods a default implementation, virtual subclasses with register(), the collections.abc ABCs that save you from writing boilerplate, and when a Protocol is the better choice.
The Problem ABCs Solve
Here's the common pre-ABC pattern: a base class with methods that raise NotImplementedError.
class Exporter:
def export(self, rows: list[dict]) -> str:
raise NotImplementedError
class JsonExporter(Exporter):
pass # forgot to implement export()
exporter = JsonExporter() # works fine
exporter.export([]) # NotImplementedError, much later
Nothing stops you from creating a JsonExporter. The bug only surfaces when someone finally calls export(), which might be in production. An ABC catches the mistake at construction time instead.
Defining an Abstract Base Class
Inherit from ABC and mark the required methods with @abstractmethod:
# exporters.py
from abc import ABC, abstractmethod
class Exporter(ABC):
@abstractmethod
def export(self, rows: list[dict]) -> str:
"""Turn rows into a string in some format."""
def export_to_file(self, rows: list[dict], path: str) -> None:
with open(path, "w", encoding="utf-8") as f:
f.write(self.export(rows))
class CsvExporter(Exporter):
def export(self, rows: list[dict]) -> str:
if not rows:
return ""
header = ",".join(rows[0])
lines = [",".join(str(v) for v in row.values()) for row in rows]
return "\n".join([header, *lines])
class BrokenExporter(Exporter):
pass
Three things are going on here:
Exporterdeclares the contract: every exporter must have anexport()method.export_to_file()is a normal, concrete method. ABCs can contain as much real code as you like, and subclasses inherit it. This is the main difference from a pure "interface" in languages like Java.BrokenExporterinherits the contract but doesn't fulfil it.
Now try to create each one:
try:
Exporter()
except TypeError as e:
print(e)
try:
BrokenExporter()
except TypeError as e:
print(e)
print(CsvExporter().export([{"id": 1, "name": "Ada"}, {"id": 2, "name": "Grace"}]))
Can't instantiate abstract class Exporter without an implementation for abstract method 'export'
Can't instantiate abstract class BrokenExporter without an implementation for abstract method 'export'
id,name
1,Ada
2,Grace
The abstract class itself can't be instantiated, the incomplete subclass can't be instantiated, and the complete subclass works. The error message names exactly which methods are missing, which makes the fix obvious.
How Python Tracks Abstract Methods
When the class is created, ABCMeta (the metaclass behind ABC) collects every name still marked abstract into a frozenset called __abstractmethods__. Instantiation fails if that set isn't empty.
print(Exporter.__abstractmethods__)
print(CsvExporter.__abstractmethods__)
frozenset({'export'})
frozenset()
ABC is just a convenience class whose metaclass is ABCMeta. These two definitions are equivalent:
from abc import ABC, ABCMeta, abstractmethod
class Base1(ABC):
@abstractmethod
def go(self) -> None: ...
class Base2(metaclass=ABCMeta):
@abstractmethod
def go(self) -> None: ...
Use ABC unless you need to combine it with another metaclass. If you're curious what a metaclass actually does, see Metaclasses in Python Explained Without the Headache.
One subtlety: the check only happens when the class is created. If you mark a method abstract after the fact (by assigning to the class attribute), the class won't notice unless you call abc.update_abstractmethods(cls), added in Python 3.10. You'll rarely need it outside of class decorators that generate methods.
Abstract Properties, Class Methods, and Static Methods
@abstractmethod stacks with the other method decorators. The rule is that @abstractmethod must be the innermost decorator, directly above the def:
from abc import ABC, abstractmethod
class Shape(ABC):
@property
@abstractmethod
def area(self) -> float: ...
@classmethod
@abstractmethod
def from_string(cls, text: str) -> "Shape": ...
@staticmethod
@abstractmethod
def kind() -> str: ...
class Square(Shape):
def __init__(self, side: float) -> None:
self.side = side
@property
def area(self) -> float:
return self.side ** 2
@classmethod
def from_string(cls, text: str) -> "Square":
return cls(float(text))
@staticmethod
def kind() -> str:
return "square"
sq = Square.from_string("3")
print(sq.area, sq.kind()) # 9.0 square
Older code sometimes uses abstractproperty, abstractclassmethod, and abstractstaticmethod. Those have been deprecated since Python 3.3; stacking decorators as shown above is the modern form.
Missing any of them still blocks instantiation, and the error lists all of the gaps at once:
class Half(Shape):
@property
def area(self) -> float:
return 1.0
Half()
TypeError: Can't instantiate abstract class Half without an implementation for abstract methods 'from_string', 'kind'
The abstract property is a good fit when you want every subclass to expose a value but don't care whether it's computed or stored. A subclass can satisfy it with a @property, as Square does. For more on properties themselves, see @property in Python: Getters, Setters, and Computed Attributes.
Abstract Methods Can Have a Body
An abstract method doesn't have to be empty. You can put shared logic in it, and subclasses call it with super(). The subclass is still forced to override the method, but it gets a useful starting point.
from abc import ABC, abstractmethod
class Validator(ABC):
@abstractmethod
def validate(self, value: str) -> list[str]:
errors = []
if not value.strip():
errors.append("value is blank")
return errors
class EmailValidator(Validator):
def validate(self, value: str) -> list[str]:
errors = super().validate(value)
if "@" not in value:
errors.append("missing @")
return errors
print(EmailValidator().validate(" "))
print(EmailValidator().validate("ada@example.com"))
['value is blank', 'missing @']
[]
This pattern says "you must think about validation for your type, and here's the baseline check everyone needs." It's cleaner than a separate _base_validate() helper that subclasses might forget to call.
The Template Method Pattern
ABCs pair naturally with the template method pattern: the base class defines an algorithm in a concrete method and leaves specific steps abstract. export_to_file() in the first example is a tiny version of this. Here's a fuller one:
# reports.py
from abc import ABC, abstractmethod
class Report(ABC):
def render(self) -> str:
parts = [self.header(), *self.body_lines(), self.footer()]
return "\n".join(parts)
@abstractmethod
def header(self) -> str: ...
@abstractmethod
def body_lines(self) -> list[str]: ...
def footer(self) -> str:
return "-- end of report --"
class SalesReport(Report):
def __init__(self, totals: dict[str, float]) -> None:
self.totals = totals
def header(self) -> str:
return "SALES"
def body_lines(self) -> list[str]:
return [f"{region}: {amount:,.2f}" for region, amount in self.totals.items()]
print(SalesReport({"EU": 12500.5, "US": 9800}).render())
SALES
EU: 12,500.50
US: 9,800.00
-- end of report --
render() controls the order of operations. Subclasses fill in header() and body_lines(), and they may override footer() but don't have to. The ABC guarantees that every report subclass supplies the pieces render() depends on.
Virtual Subclasses with register()
Sometimes you want a class you don't control, such as one from a third-party library, to count as an instance of your ABC. Subclassing isn't an option, so ABCs offer register():
from abc import ABC, abstractmethod
class Plugin(ABC):
@abstractmethod
def run(self) -> None: ...
class LegacyPlugin:
def run(self) -> None:
print("legacy running")
Plugin.register(LegacyPlugin)
print(isinstance(LegacyPlugin(), Plugin)) # True
print(issubclass(LegacyPlugin, Plugin)) # True
print(Plugin in LegacyPlugin.__mro__) # False
LegacyPlugin is now a virtual subclass. isinstance() and issubclass() say yes, but Plugin doesn't appear in its MRO, so it inherits nothing: no concrete methods, no super() chain.
register() also works as a class decorator, and here's the catch worth knowing:
@Plugin.register
class OtherPlugin:
pass
print(isinstance(OtherPlugin(), Plugin)) # True, even though run() is missing
Registration is a promise you make, not something Python verifies. OtherPlugin has no run() method at all, and it still passes the isinstance check and can be instantiated. Use register() only for classes you know conform.
Structural Checks with __subclasshook__
An ABC can also decide membership by inspecting a class, using the __subclasshook__ class method. This is how collections.abc.Iterable recognises any class with an __iter__ method.
import io
from abc import ABC, abstractmethod
class Closeable(ABC):
@abstractmethod
def close(self) -> None: ...
@classmethod
def __subclasshook__(cls, C):
if cls is Closeable:
if any("close" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented
print(isinstance(io.StringIO(), Closeable)) # True
print(isinstance("text", Closeable)) # False
The hook returns True (it's a subclass), False (it definitely isn't), or NotImplemented (fall back to the normal checks). The cls is Closeable guard stops the hook from applying to subclasses of Closeable, which might have stricter requirements. In modern code you'll write this less often, because typing.Protocol covers the same need more cleanly (more on that below).
Using collections.abc
The most practical ABCs are the ones you don't write. collections.abc defines the standard container interfaces: Iterable, Iterator, Sized, Container, Sequence, MutableSequence, Mapping, MutableMapping, Set, Callable, Hashable, and more.
Checking Capabilities
Many of these use __subclasshook__, so you get duck-typed checks for free:
from collections.abc import Callable, Hashable, Iterable, Mapping, Sized
class Countdown:
def __init__(self, start: int) -> None:
self.start = start
def __iter__(self):
return iter(range(self.start, 0, -1))
def __len__(self) -> int:
return self.start
c = Countdown(3)
print(isinstance(c, Iterable), isinstance(c, Sized), isinstance(c, Mapping))
print(isinstance([], Hashable), isinstance(len, Callable))
True True False
False True
Countdown never mentions Iterable or Sized, but it has __iter__ and __len__, so it qualifies. A list isn't Hashable because it sets __hash__ to None. Checking isinstance(x, Iterable) is usually better than hasattr(x, "__iter__") because it reads like intent.
Mapping has no hook, so Countdown isn't one: you have to subclass or register to become a Mapping.
Getting Mixin Methods for Free
This is where collections.abc really pays off. Subclass Mapping, implement three methods, and you get get(), keys(), items(), values(), __contains__, and __eq__ automatically:
from collections.abc import Iterator, Mapping
class Settings(Mapping[str, str]):
def __init__(self, data: dict[str, str]) -> None:
self._data = dict(data)
def __getitem__(self, key: str) -> str:
return self._data[key]
def __iter__(self) -> Iterator[str]:
return iter(self._data)
def __len__(self) -> int:
return len(self._data)
s = Settings({"env": "prod", "region": "eu"})
print(s.get("env"), s.get("missing", "n/a"), "region" in s)
print(list(s.keys()), list(s.items()))
print(s == {"env": "prod", "region": "eu"})
prod n/a True
['env', 'region'] [('env', 'prod'), ('region', 'eu')]
True
You get a read-only, dict-like object in about a dozen lines. Forget one of the required methods and you get the familiar error:
TypeError: Can't instantiate abstract class Bad without an implementation for abstract methods '__iter__', '__len__'
The collections.abc documentation has a table listing, for each ABC, which abstract methods you must write and which mixin methods you get in return. It's worth bookmarking.
Since Python 3.9 these classes are subscriptable (Mapping[str, str]), which is why they're also the recommended way to write container type hints. Prefer collections.abc.Iterable over the deprecated typing.Iterable alias.
ABCs vs Protocols
Python 3.8 added typing.Protocol, which describes an interface structurally: any class with matching methods satisfies it, no inheritance required.
import io
from typing import Protocol, runtime_checkable
@runtime_checkable
class SupportsClose(Protocol):
def close(self) -> None: ...
print(isinstance(io.StringIO(), SupportsClose)) # True
So which should you use?
| Need | ABC | Protocol |
|---|---|---|
| Block instantiation of incomplete subclasses | Yes | No |
| Share concrete methods with implementers | Yes | Not intended for it |
| Accept classes you don't own without changes | Only with register() | Yes, automatically |
| Checked by type checkers (mypy, Pyright) | Yes, as nominal types | Yes, structurally |
Runtime isinstance() | Yes | Only with @runtime_checkable, and only checks names exist |
A reasonable rule: use an ABC when you own the hierarchy and want to share behaviour and enforce completeness at runtime (plugins, exporters, storage backends). Use a Protocol when you just want to describe "anything with a close() method" for type checking, especially across library boundaries. The Generics, Protocols, and TypedDict post goes deeper on protocols.
Common Pitfalls
- Forgetting to inherit from
ABC.@abstractmethodon a regular class does nothing at runtime. The decorator sets a flag;ABCMetais what enforces it. - Wrong decorator order.
@abstractmethodmust sit closest to thedef. Putting@propertyunderneath it fails as soon as the class is defined, withAttributeError: attribute '__isabstractmethod__' of 'property' objects is not writable. - Expecting signature checks. ABCs check that a method with the right name exists. They don't check parameters or return types. A subclass can define
export(self)with no arguments and Python won't complain; a type checker will. - Metaclass conflicts. If you combine an ABC with a class that uses a different metaclass (some ORMs and frameworks do), you'll get a "metaclass conflict" error. The fix is a combined metaclass that inherits from both.
- Overusing them. Not every base class needs to be abstract. If there's only one implementation and no realistic second one, a plain class is simpler.
Conclusion
Abstract base classes turn "every subclass should implement this" from a comment into a rule Python enforces when an object is created. Inherit from ABC, mark required methods with @abstractmethod (innermost when stacking with @property or @classmethod), and put shared logic in concrete methods or in the abstract method's body for subclasses to reach with super().
Beyond your own hierarchies, collections.abc is the part of this module you'll likely use most: it gives you capability checks like isinstance(x, Iterable) and lets you build full container types from a handful of methods. When you don't need runtime enforcement or shared code, reach for typing.Protocol instead.


