Python abc: Abstract Base Classes

Published on: August 6, 2026
Reading time: 5 minutes
Software architecture representing abstract base classes with Python abc

Object-oriented projects often need a common contract for several implementations. A payment class must expose process(); a repository needs save() and get(); a plugin must declare required lifecycle methods. The Python abc module provides abstract base classes, mandatory methods, and virtual subclasses for expressing these contracts at runtime.

This guide covers ABC, ABCMeta, abstractmethod, register(), __subclasshook__(), and update_abstractmethods(). It complements our articles about Python types, inspect, singledispatch, descriptors, and dataclasses.

What an abstract class is

An abstract class describes expected behavior and may provide partial implementation, but it cannot be instantiated while unresolved abstract members remain.

from abc import ABC, abstractmethod

class Storage(ABC):
    @abstractmethod
    def save(self, key, value):
        pass

    @abstractmethod
    def get(self, key):
        pass

Calling Storage() raises TypeError. A concrete subclass must implement every abstract member.

Create a concrete implementation

class MemoryStorage(Storage):
    def __init__(self):
        self._data = {}

    def save(self, key, value):
        self._data[key] = value

    def get(self, key):
        return self._data[key]

repository = MemoryStorage()
repository.save("user", "Ana")
print(repository.get("user"))

The base class documents the contract and prevents accidental construction of incomplete implementations.

ABC is a helper class

ABC uses ABCMeta as its metaclass. Inheriting from ABC is the simplest way to declare an abstract base class.

A class that already needs another metaclass can encounter a conflict. Derive a compatible metaclass from ABCMeta or reconsider a complex inheritance design.

abstractmethod

@abstractmethod marks required behavior. It must be the innermost decorator when combined with classmethod, staticmethod, or property.

class Factory(ABC):
    @classmethod
    @abstractmethod
    def create(cls, configuration):
        pass

    @staticmethod
    @abstractmethod
    def validate(configuration):
        pass

The order matters because abstractmethod marks the underlying descriptor.

Abstract properties

class Document(ABC):
    @property
    @abstractmethod
    def title(self):
        pass

A subclass implements another property.

class Report(Document):
    def __init__(self, title):
        self._title = title

    @property
    def title(self):
        return self._title

Older helpers such as abstractproperty, abstractclassmethod, and abstractstaticmethod remain for compatibility, but modern decorator combinations are preferred.

Abstract methods may have implementations

An abstract method does not need to contain only pass. It can provide common behavior that subclasses call through super().

class Exporter(ABC):
    @abstractmethod
    def export(self, data):
        self._validate(data)

    def _validate(self, data):
        if not data:
            raise ValueError("empty data")

class JSONExporter(Exporter):
    def export(self, data):
        super().export(data)
        return json.dumps(data)

This creates a cooperative endpoint for multiple inheritance and avoids duplicated validation.

Partial implementations

A subclass may remain abstract.

class LoggingStorage(Storage):
    def save(self, key, value):
        print("saving", key)
        return self._save_impl(key, value)

    @abstractmethod
    def _save_impl(self, key, value):
        pass

It implements part of the original contract and introduces a new obligation for concrete descendants.

Inspect abstract state

The metaclass maintains __abstractmethods__.

print(Storage.__abstractmethods__)
print(MemoryStorage.__abstractmethods__)

Frameworks can use inspect.isabstract() for a clearer public API.

ABCMeta directly

from abc import ABCMeta

class Service(metaclass=ABCMeta):
    @abstractmethod
    def run(self):
        pass

Explicit metaclass syntax helps when combining metaclasses, but inheriting from ABC is usually more readable.

Virtual subclasses

register() can treat an existing class as a subclass without modifying its inheritance.

class Reader(ABC):
    @abstractmethod
    def read(self):
        pass

class LegacyReader:
    def read(self):
        return "data"

Reader.register(LegacyReader)

print(issubclass(LegacyReader, Reader))
print(isinstance(LegacyReader(), Reader))

Registration does not add methods, call ABC code, or enforce the contract. It only changes issubclass() and isinstance() results.

Virtual-subclass risks

Registering an incompatible class creates a false sense of conformance.

class Incomplete:
    pass

Reader.register(Incomplete)
print(isinstance(Incomplete(), Reader))  # True

Register only classes that already satisfy the contract semantically, and keep shared conformance tests.

register() as a decorator

@Reader.register
class OtherReader:
    def read(self):
        return "other"

This keeps the intent near the implementation but also creates a direct dependency on the ABC.

__subclasshook__()

An ABC can recognize structural subclasses without explicit registration.

class CustomIterable(ABC):
    @classmethod
    def __subclasshook__(cls, C):
        if cls is CustomIterable:
            if any("__iter__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

Classes that define __iter__ in their hierarchy can now satisfy issubclass().

True, False, or NotImplemented

__subclasshook__() may return:

  • True to accept the class;
  • False to reject it decisively;
  • NotImplemented to continue normal checks.

Prefer NotImplemented when the hook cannot decide. Returning false is a strong override.

Keep hooks conservative

The presence of a name does not prove a compatible signature or meaning. A method named read can perform unrelated work.

Use hooks for well-known, easily recognized protocols. Rich contracts are safer with explicit inheritance, Protocol, and tests.

ABC versus Protocol

ABCs focus on nominal runtime relationships and shared implementation. typing.Protocol expresses structural typing for static checkers.

from typing import Protocol

class CanRead(Protocol):
    def read(self) -> str: ...

A class with a compatible method satisfies this protocol without inheritance. Runtime-checkable protocols offer limited runtime tests but do not deeply validate signatures.

ABC versus duck typing

Python can simply call obj.read() and handle AttributeError. This reduces coupling.

Use an ABC when central documentation, shared implementation, blocked construction of incomplete classes, or runtime registration provides concrete value.

Multiple inheritance

class Readable(ABC):
    @abstractmethod
    def read(self): ...

class Writable(ABC):
    @abstractmethod
    def write(self, data): ...

class VirtualFile(Readable, Writable):
    def read(self):
        return self.data

    def write(self, data):
        self.data = data

Keep cooperative methods using super() and avoid conflicting state in base classes.

Update abstract methods dynamically

Adding an implementation after class creation does not automatically recalculate abstract status. Use update_abstractmethods().

from abc import update_abstractmethods

class Dynamic(ABC):
    @abstractmethod
    def run(self):
        pass

Dynamic.run = lambda self: "ok"
update_abstractmethods(Dynamic)

print(Dynamic().run())

The official abc documentation recommends this helper for dynamic modification and assumes base classes were already updated.

ABC caches

ABCMeta caches subclass checks. get_cache_token() returns a token that changes when virtual-subclass registrations change.

from abc import get_cache_token

before = get_cache_token()
Reader.register(NewReader)
after = get_cache_token()
assert before != after

Frameworks with their own conformance caches can invalidate them when the token changes.

Plugin contracts

class Plugin(ABC):
    name: str

    @abstractmethod
    def start(self, context):
        pass

    @abstractmethod
    def stop(self):
        pass

When loading a plugin, confirm that the class is concrete, validate metadata, and handle startup failures. Inheritance provides no security isolation.

Dependencies and tests

Receiving an ABC makes a dependency contract explicit.

class Application:
    def __init__(self, storage: Storage):
        self.storage = storage

Tests can provide a small fake implementation. Avoid unrestricted mocks that silently accept misspelled methods.

Evolving the API

Adding a new abstract method breaks every existing concrete subclass. It is a backward-incompatible change.

Consider a default implementation, a new ABC, or a separate capability interface before making new behavior mandatory.

Common mistakes

  • Placing @abstractmethod outside @classmethod or @property.
  • Assuming a virtual subclass receives methods.
  • Registering classes that do not satisfy the contract.
  • Writing overly broad structural hooks.
  • Adding abstract methods without compatibility planning.
  • Building deep hierarchies to share little code.
  • Treating an ABC as plugin isolation.
  • Modifying a class without update_abstractmethods().

Best practices

  • Keep contracts small and cohesive.
  • Provide common implementation where useful.
  • Use super() in cooperative inheritance.
  • Register virtual subclasses only after tests.
  • Return NotImplemented when hooks are uncertain.
  • Compare ABCs with Protocols and duck typing.
  • Treat new abstract members as breaking changes.
  • Run one shared conformance suite for implementations.

Conclusion

The Python abc module defines runtime contracts, prevents construction of incomplete classes, and shares behavior among implementations. ABC and abstractmethod cover most cases, while virtual registration and __subclasshook__() support structural integration.

Abstract classes work best when contracts are small, stable, and semantically clear. Conservative hooks, conformance tests, and careful API evolution keep extensions explicit without turning a project into a rigid hierarchy.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Code and structures representing runtime types with the Python types module
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python types: Runtime Type Utilities

    Learn Python types for SimpleNamespace, MappingProxyType, runtime type names, and safe dynamic class creation.

    Ler mais

    Tempo de leitura: 5 minutos
    06/08/2026
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python sched: Schedule Events

    Learn Python sched to schedule events, control priorities, cancel tasks, and build recurring work with a monotonic clock.

    Ler mais

    Tempo de leitura: 7 minutos
    05/08/2026
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python atexit: Run Cleanup on Exit

    Learn Python atexit to run cleanup at shutdown, control LIFO order, and avoid problems with threads, signals, and handler exceptions.

    Ler mais

    Tempo de leitura: 6 minutos
    05/08/2026
    Monitor with binary code representing pickle opcode analysis with Python pickletools
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python pickletools: Analyze Pickles

    Learn Python pickletools to disassemble pickles, inspect opcodes, and optimize streams without executing untrusted data.

    Ler mais

    Tempo de leitura: 6 minutos
    05/08/2026
    Source code on screen representing analysis with Python tokenize
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python tokenize: Analyze Source Code

    Learn Python tokenize to inspect tokens, comments, indentation, encodings, source positions, and rebuild code safely.

    Ler mais

    Tempo de leitura: 6 minutos
    05/08/2026
    Developer working on build automation and directory compilation with Python compileall
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python compileall: Compile Directories

    Learn Python compileall to compile directories, generate pyc files in parallel, filter paths, and control optimization and invalidation.

    Ler mais

    Tempo de leitura: 6 minutos
    04/08/2026