The abc module declares classes that cannot be instantiated until required operations are implemented. It makes extension points explicit, but does not justify an inheritance hierarchy for every variation.

Declare a contract

from abc import ABC, abstractmethod
from decimal import Decimal


class Payment(ABC):
    @abstractmethod
    def charge(self, amount: Decimal) -> str:
        """Return the charge identifier."""


class FakePayment(Payment):
    def charge(self, amount: Decimal) -> str:
        if amount <= 0:
            raise ValueError("amount must be positive")
        return "fake-1"

Instantiating a subclass without charge raises TypeError. ABCs and annotations do not validate the positive amount rule; the concrete behavior still owns it.

ABC, Protocol, or composition

Use an ABC when subclasses belong to a nominal family and may share behavior. Choose typing.Protocol when objects only need matching methods without inheriting a base. Use composition when a class merely needs a collaborator.

Avoid ABCs with many methods, empty implementations, or subclasses that reject half the contract. Those are signs of oversized responsibilities. A shared contract test can verify every implementation.

What abstractmethod actually guarantees

The decorator records a method as abstract. As long as at least one abstract method has no concrete override, the metaclass prevents instantiation. The check happens when creating an object, not when calling the method, so an incomplete implementation fails early:

class IncompletePayment(Payment):
    pass


## TypeError: Can't instantiate abstract class ...
IncompletePayment()

The annotated signature guides readers and static checkers, but the ABC does not automatically compare every parameter in an override. Use a type checker to catch incompatible signatures. Tests must still verify semantics, exceptions, and side effects.

An abstract method may contain code. This can support cooperative inheritance when the contract clearly requires subclasses to call super():

class Exporter(ABC):
    @abstractmethod
    def export(self, rows: list[str]) -> bytes:
        if not rows:
            raise ValueError("at least one row is required")
        return b""


class TextExporter(Exporter):
    def export(self, rows: list[str]) -> bytes:
        super().export(rows)
        return "\n".join(rows).encode("utf-8")

Despite having an implementation, Exporter.export remains abstract. A subclass must override it before becoming concrete. Use this carefully: shared validation in a separate concrete helper is often easier to understand.

Abstract properties and class methods

abstractmethod can be combined with descriptors. Place it closest to the function, inside the other decorator:

class Repository(ABC):
    @property
    @abstractmethod
    def name(self) -> str:
        ...

    @classmethod
    @abstractmethod
    def from_url(cls, url: str) -> "Repository":
        ...

A concrete implementation can satisfy name with a property and from_url with a class method. Do not make every internal detail mandatory. The contract should expose only what consumers need.

Virtual subclasses and __subclasshook__

MyABC.register(SomeType) registers a virtual subclass. Afterwards, issubclass(SomeType, MyABC) and isinstance(value, MyABC) are true even though SomeType inherits no implementation and is not forced to override abstract methods. Registration helps integrate an external class into a nominal category, but it can imply a stronger guarantee than it provides.

__subclasshook__ lets an ABC recognize classes structurally during issubclass. It is an advanced tool for library ABCs. In application code, Protocol usually communicates structural typing more clearly and supports static checking. Neither feature proves behavior: a charge method does not guarantee idempotency, correct currency handling, or useful failure modes.

A shared contract test

Every implementation should pass the same essential scenarios. A reusable test function catches subclasses that satisfy only the signature:

def verify_payment(payment: Payment) -> None:
    identifier = payment.charge(Decimal("10.00"))
    assert identifier

    try:
        payment.charge(Decimal("0"))
    except ValueError:
        pass
    else:
        raise AssertionError("zero should be rejected")

Run it against production adapters and in-memory fakes. For real integrations, document observable rules such as retries, timeouts, and error translation. Avoid testing implementation details that would exclude valid alternatives.

When an ABC improves a design

An ABC fits when the application controls a family of implementations, the domain role is stable, and consumers need to depend on that role. Storage adapters, pricing strategies, and exporters are common examples. A base can also provide concrete behavior such as normalization, logging, or a template method that invokes abstract steps.

It is a poor choice when there is only one implementation, the abstraction predicts hypothetical needs, or subclasses merely share code without being substitutable. Start with a concrete class, extract a function, or inject a callable instead. The abstraction should emerge from a relationship that consumers actually use.

Common mistakes

Do not confuse @abstractmethod with a method that merely raises NotImplementedError. The latter permits instantiation and fails later. Avoid abstract constructors with many implementation-specific parameters too; subclasses may require different dependencies.

Multiple inheritance requires careful method resolution and consistent super() calls. Composition is more predictable when bases are not cooperative. Finally, do not scatter isinstance checks against the ABC to select behavior. Polymorphism means calling the contract; frequent type branches suggest that the abstraction is missing an operation.

Evolving a contract safely

Adding an abstract method to a published ABC makes every existing subclass incomplete. Before changing the contract, find implementations inside and outside the repository and consider compatibility. A concrete method with a safe default may support gradual migration. Another option is a smaller ABC for the new capability, allowing only relevant consumers to require it.

Separating reads from writes can help. A ReadRepository with get works for reports and read-only caches, while WriteRepository declares save. Consumers needing both may accept a combined ABC. This segregation avoids forcing legitimate adapters to add fake methods.

Document more than the signature: state whether a method mutates state, which exceptions belong to the API, whether repeated calls are safe, and which resources callers must release. The metaclass cannot verify these rules, but they determine whether implementations are genuinely substitutable.

For diagnosis, inspect Class.__abstractmethods__, an immutable set of names still considered abstract. It can explain an instantiation TypeError, but should not drive business logic. If decorators modify methods after class creation, abc.update_abstractmethods() recalculates the set. That uncommon technique deserves focused tests.

Keep dependency annotations at the smallest useful contract. A function that only calls charge should accept Payment, not a particular gateway adapter. Its unit test can then supply a small fake. This is the practical payoff of the ABC: consumers describe what they need while deployment code selects the concrete implementation. Construct and wire adapters near the application's entry point so domain code remains unaware of credentials, HTTP clients, and storage configuration.

The SOLID in Python guide helps evaluate substitution and segregation. The official abc documentation, accessed July 22, 2026, covers ABC, abstractmethod, virtual subclasses, and inheritance details. Prefer the smallest contract grounded in a real need.