El módulo abc permite declarar clases que no pueden instanciarse hasta implementar operaciones obligatorias. Hace explícitos los puntos de extensión, pero no justifica una jerarquía para cada variación.

Declarar un contrato

from abc import ABC, abstractmethod
from decimal import Decimal


class Pago(ABC):
    @abstractmethod
    def cobrar(self, importe: Decimal) -> str:
        """Devuelve el identificador del cobro."""


class PagoFalso(Pago):
    def cobrar(self, importe: Decimal) -> str:
        if importe <= 0:
            raise ValueError("el importe debe ser positivo")
        return "falso-1"

Instanciar una subclase sin cobrar produce TypeError. La ABC y las anotaciones no validan la regla del importe positivo; corresponde al comportamiento concreto.

ABC, Protocol o composición

Usa ABC si las subclases forman una familia nominal y comparten comportamiento. Prefiere typing.Protocol cuando basta con métodos compatibles sin heredar una base. Usa composición si una clase solo necesita un colaborador.

Evita ABC con muchos métodos, implementaciones vacías o subclases que rechazan medio contrato. Son señales de responsabilidades excesivas. Una suite compartida puede probar todas las implementaciones.

Qué garantiza realmente abstractmethod

El decorador registra el método como abstracto. Mientras quede al menos uno sin implementación concreta, la metaclase impide instanciar la clase. La comprobación sucede al crear el objeto, no al invocar el método, por lo que una implementación incompleta falla pronto:

class PagoIncompleto(Pago):
    pass


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

La firma anotada orienta a lectores y verificadores estáticos, pero la ABC no compara automáticamente todos los parámetros de una sobrescritura. Un verificador detecta firmas incompatibles. Las pruebas siguen siendo necesarias para comprobar semántica, excepciones y efectos secundarios.

Un método abstracto puede contener código. Es útil para herencia cooperativa si el contrato indica que la subclase debe llamar a super():

class Exportador(ABC):
    @abstractmethod
    def exportar(self, filas: list[str]) -> bytes:
        if not filas:
            raise ValueError("se necesita al menos una fila")
        return b""


class ExportadorTexto(Exportador):
    def exportar(self, filas: list[str]) -> bytes:
        super().exportar(filas)
        return "\n".join(filas).encode("utf-8")

Aunque tenga implementación, Exportador.exportar continúa siendo abstracto. La subclase debe sobrescribirlo para ser concreta. Usa esta técnica con moderación: una validación compartida en un método concreto separado suele resultar más clara.

Propiedades y métodos de clase abstractos

abstractmethod se combina con otros descriptores. Debe quedar junto a la función, dentro del otro decorador:

class Repositorio(ABC):
    @property
    @abstractmethod
    def nombre(self) -> str:
        ...

    @classmethod
    @abstractmethod
    def desde_url(cls, url: str) -> "Repositorio":
        ...

Una implementación concreta puede satisfacer nombre con una propiedad y desde_url con un método de clase. No conviertas cada detalle interno en requisito. El contrato debe exponer solo lo que necesitan sus consumidores.

Subclases virtuales y __subclasshook__

MiABC.register(Tipo) registra una subclase virtual. Después, issubclass(Tipo, MiABC) e isinstance(valor, MiABC) devuelven True, aunque Tipo no herede código ni esté obligado a implementar los métodos abstractos. El registro integra clases externas en una categoría nominal, pero puede sugerir una garantía mayor de la real.

__subclasshook__ permite reconocer clases por su estructura durante issubclass. Es una herramienta avanzada para ABC de bibliotecas. En aplicaciones, Protocol suele expresar mejor el tipado estructural y admite análisis estático. Ninguna opción demuestra comportamiento: tener cobrar no garantiza idempotencia, moneda correcta ni tratamiento útil de fallos.

Una prueba de contrato compartida

Todas las implementaciones deben superar los mismos escenarios esenciales. Una función reutilizable detecta subclases que cumplen solo la firma:

def verificar_pago(pago: Pago) -> None:
    identificador = pago.cobrar(Decimal("10.00"))
    assert identificador

    try:
        pago.cobrar(Decimal("0"))
    except ValueError:
        pass
    else:
        raise AssertionError("el importe cero debe rechazarse")

Ejecútala para adaptadores de producción y dobles en memoria. En integraciones reales, documenta reglas observables como reintentos, timeout y traducción de errores. No impongas detalles internos que excluyan alternativas válidas.

Cuándo mejora el diseño

Una ABC encaja cuando la aplicación controla una familia de implementaciones, el papel de dominio es estable y los consumidores necesitan depender de ese papel. Adaptadores de almacenamiento, estrategias de precios y exportadores son ejemplos habituales. La base también puede aportar normalización, logging o un método plantilla que invoque pasos abstractos.

Es una mala elección si solo hay una implementación, si anticipa necesidades hipotéticas o si las subclases comparten código sin ser sustituibles. Empieza con una clase concreta, extrae una función o inyecta un callable. La abstracción debe surgir de una relación que los consumidores usan.

Errores frecuentes

No confundas @abstractmethod con un método que solo lanza NotImplementedError. El segundo permite instanciar la clase y falla tarde. Evita también constructores abstractos con muchos parámetros específicos: las subclases quizá necesiten dependencias distintas.

La herencia múltiple exige cuidar el orden de resolución y usar super() de forma consistente. Si las bases no cooperan, la composición es más predecible. Tampoco disperses comprobaciones isinstance contra la ABC para elegir comportamientos. El polimorfismo consiste en invocar el contrato; muchas ramas por tipo indican que falta una operación.

Evolucionar un contrato sin romper implementaciones

Añadir un método abstracto a una ABC publicada deja incompletas todas las subclases existentes. Antes de modificarla, localiza implementaciones dentro y fuera del repositorio y considera la compatibilidad. Un método concreto con un valor seguro por defecto puede facilitar una migración gradual. Otra opción es crear una ABC pequeña para la capacidad nueva.

También conviene separar lectura y escritura. Un RepositorioLectura con buscar sirve para informes y cachés de solo lectura; RepositorioEscritura declara guardar. Los consumidores que necesitan ambos pueden aceptar una ABC combinada. Así no obligas a adaptadores válidos a inventar métodos.

Documenta más que la firma: especifica si modifica estado, qué excepciones pertenecen a la API, si admite llamadas repetidas y qué recursos debe liberar el cliente. La metaclase no verifica estas reglas, pero determinan si una implementación sustituye a otra.

Para diagnosticar, consulta Clase.__abstractmethods__, un conjunto inmutable con los nombres todavía abstractos. Ayuda a explicar un TypeError, pero no debe dirigir reglas de negocio. Si unos decoradores modifican métodos después de crear la clase, abc.update_abstractmethods() recalcula el conjunto. Es una técnica infrecuente que exige pruebas específicas.

Anota las dependencias con el contrato mínimo útil. Una función que solo llama a cobrar debe aceptar Pago, no un adaptador concreto. Su prueba puede proporcionar un doble pequeño. Ese es el beneficio práctico de la ABC: los consumidores expresan lo que necesitan y el arranque de la aplicación elige la implementación. Construye adaptadores cerca del punto de entrada para que el dominio no conozca credenciales, clientes HTTP ni configuración de almacenamiento.

La guía de SOLID en Python ayuda a evaluar sustitución y segregación. La documentación oficial de abc, consultada el 22 de julio de 2026, cubre ABC, abstractmethod, subclases virtuales y herencia. Prefiere el contrato mínimo que represente una necesidad real.