Type something to search...
Abstract Base Classes in Python with the abc Module

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:

  • Exporter declares the contract: every exporter must have an export() 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.
  • BrokenExporter inherits 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?

NeedABCProtocol
Block instantiation of incomplete subclassesYesNo
Share concrete methods with implementersYesNot intended for it
Accept classes you don't own without changesOnly with register()Yes, automatically
Checked by type checkers (mypy, Pyright)Yes, as nominal typesYes, structurally
Runtime isinstance()YesOnly 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. @abstractmethod on a regular class does nothing at runtime. The decorator sets a flag; ABCMeta is what enforces it.
  • Wrong decorator order. @abstractmethod must sit closest to the def. Putting @property underneath it fails as soon as the class is defined, with AttributeError: 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.

Tags :
Share :

Related Posts

*args and **kwargs in Python: Flexible Function Signatures

*args and **kwargs in Python: Flexible Function Signatures

You've seen def wrapper(*args, **kwargs): in decorators, and probably super().__init__(**kwargs) in class hierarchies. These two parameters let a

Continue Reading
Asyncio in Python: A Beginner's Guide to Asynchronous Programming

Asyncio in Python: A Beginner's Guide to Asynchronous Programming

A lot of programs spend most of their time waiting. A web scraper waits for pages to download, an API server waits for the database, a chat bot waits

Continue Reading
Authentication in FastAPI with OAuth2 and JWT

Authentication in FastAPI with OAuth2 and JWT

Most APIs need to know who's calling them. FastAPI doesn't ship a complete user system, but it gives you well-designed building blocks: OAuth2 helper

Continue Reading