O módulo abc permite declarar classes que não devem ser instanciadas até que operações obrigatórias sejam implementadas. Isso torna uma extensão explícita, mas não justifica criar hierarquias para qualquer variação.
Declarar um contrato
from abc import ABC, abstractmethod
from decimal import Decimal
class Pagamento(ABC):
@abstractmethod
def cobrar(self, valor: Decimal) -> str:
"""Retorna o identificador da cobrança."""
class PagamentoTeste(Pagamento):
def cobrar(self, valor: Decimal) -> str:
if valor <= 0:
raise ValueError("valor deve ser positivo")
return "teste-1"
Instanciar uma subclasse que não implementa cobrar gera TypeError. Ainda assim, anotações e ABC não validam regras como valor positivo; isso pertence ao comportamento concreto.
ABC, Protocol ou composição
Use ABC quando subclasses fazem parte de uma família nominal e podem compartilhar comportamento. Prefira typing.Protocol quando basta que objetos tenham certos métodos, sem herdar de uma base. Use composição quando uma classe apenas precisa receber um colaborador.
Evite uma ABC com dezenas de métodos, implementações vazias ou subclasses que lançam NotImplementedError para metade do contrato. Esses sinais indicam responsabilidades grandes demais. Teste cada implementação com uma suíte de contrato compartilhada.
O que abstractmethod realmente garante
O decorador registra o método como abstrato na classe. Enquanto existir ao menos um método abstrato sem implementação concreta, a metaclasse impede a instanciação. A verificação ocorre ao criar o objeto, não ao chamar o método. Isso detecta cedo uma implementação incompleta:
class PagamentoIncompleto(Pagamento):
pass
## TypeError: Can't instantiate abstract class ...
PagamentoIncompleto()
A assinatura declarada também orienta leitores e verificadores estáticos, mas a ABC não compara automaticamente todos os parâmetros da sobrescrita. Um type checker ajuda a encontrar assinaturas incompatíveis. Testes continuam necessários para confirmar semântica, exceções e efeitos colaterais.
Métodos abstratos podem conter código. Isso é útil em herança cooperativa, desde que o contrato diga claramente que a subclasse deve chamar super():
class Exportador(ABC):
@abstractmethod
def exportar(self, linhas: list[str]) -> bytes:
if not linhas:
raise ValueError("é preciso informar ao menos uma linha")
return b""
class ExportadorTexto(Exportador):
def exportar(self, linhas: list[str]) -> bytes:
super().exportar(linhas)
return "\n".join(linhas).encode("utf-8")
Mesmo com uma implementação, Exportador.exportar permanece abstrato. A subclasse precisa sobrescrevê-lo para se tornar concreta. Use esse recurso com parcimônia: validação compartilhada em um método concreto separado costuma ser mais óbvia.
Propriedades, classmethods e staticmethods abstratos
abstractmethod pode ser combinado com outros descritores. Ele deve ficar mais próximo da função, por dentro dos demais decoradores:
class Repositorio(ABC):
@property
@abstractmethod
def nome(self) -> str:
...
@classmethod
@abstractmethod
def da_url(cls, url: str) -> "Repositorio":
...
Uma implementação concreta pode atender nome com uma propriedade e da_url com um método de classe. Não transforme todo detalhe interno em requisito. O contrato deve expor somente o que consumidores precisam usar.
Subclasses virtuais e __subclasshook__
MinhaABC.register(Tipo) registra uma classe como subclasse virtual. Depois disso, issubclass(Tipo, MinhaABC) e isinstance(objeto, MinhaABC) retornam True, embora Tipo não herde métodos nem seja obrigado a implementar os abstratos. O registro serve para integrar classes externas a uma categoria nominal, mas pode transmitir uma garantia maior do que realmente existe.
__subclasshook__ permite reconhecer uma classe por sua estrutura durante issubclass. É uma ferramenta avançada para ABCs de biblioteca. Em código de aplicação, Protocol geralmente comunica melhor a tipagem estrutural e pode ser verificado estaticamente. Nenhuma dessas opções valida o comportamento: ter um método chamado cobrar não garante idempotência, moeda correta ou tratamento de falhas.
Uma suíte de contrato compartilhada
Cada implementação deve passar pelos mesmos cenários essenciais. Uma função de teste reutilizável reduz a chance de uma subclasse cumprir apenas a assinatura:
def verificar_pagamento(pagamento: Pagamento) -> None:
identificador = pagamento.cobrar(Decimal("10.00"))
assert identificador
try:
pagamento.cobrar(Decimal("0"))
except ValueError:
pass
else:
raise AssertionError("valor zero deveria ser rejeitado")
Esse teste pode ser chamado para o adaptador de produção e para implementações em memória. Em integrações reais, inclua no contrato regras observáveis como repetição da operação, timeout e tradução de erros. Não imponha detalhes internos que impeçam implementações legítimas.
Quando uma ABC melhora o design
Uma ABC é adequada quando a aplicação controla uma família de implementações, existe vocabulário de domínio estável e consumidores precisam depender desse papel. Adaptadores de armazenamento, estratégias de cálculo e exportadores são exemplos comuns. Uma base também pode oferecer comportamento concreto, como normalização, logging ou um método modelo que chama etapas abstratas.
Ela é uma escolha ruim quando existe apenas uma implementação, quando a abstração antecipa necessidades hipotéticas ou quando subclasses compartilham código mas não são substituíveis. Nesses casos, comece com uma classe concreta, extraia uma função ou injete um callable. A abstração deve nascer da relação usada pelos consumidores.
Erros frequentes
Não confunda @abstractmethod com um método que apenas lança NotImplementedError. O segundo permite instanciar a classe e só falha tarde. Evite também construtores abstratos com muitos argumentos específicos: eles acoplam subclasses que talvez precisem de dependências diferentes.
Herança múltipla exige atenção à ordem de resolução de métodos e ao uso consistente de super(). Se as bases não forem cooperativas, composição é mais previsível. Por fim, não use isinstance contra a ABC em todo o código para selecionar comportamentos. Polimorfismo significa chamar o contrato; ramificações frequentes por tipo indicam que a abstração pode estar incompleta.
Evoluir um contrato sem quebrar implementações
Adicionar um método abstrato a uma ABC publicada transforma todas as subclasses existentes em incompletas. Antes de evoluir o contrato, procure implementações dentro e fora do repositório e considere compatibilidade. Um método concreto com comportamento padrão pode permitir migração gradual, desde que o padrão seja seguro. Outra opção é criar uma ABC menor para a nova capacidade e fazer consumidores dependerem dela.
Também vale separar leitura e escrita. Um RepositorioLeitura com buscar pode atender relatórios e caches somente leitura; RepositorioEscrita pode declarar salvar. Consumidores que precisam dos dois podem aceitar uma ABC composta. Essa segregação evita obrigar adaptadores legítimos a criar métodos falsos.
Documente mais que a assinatura: indique se o método modifica estado, quais exceções fazem parte da API, se aceita chamadas repetidas e que recursos o chamador deve liberar. Essas regras não são verificadas pela metaclasse, mas determinam se uma implementação pode substituir outra sem surpresas.
Para depurar, consulte Classe.__abstractmethods__, um conjunto imutável com os nomes ainda abstratos. Ele ajuda a explicar um TypeError, mas não deve dirigir lógica de negócio. Se decoradores modificarem métodos depois da criação da classe, abc.update_abstractmethods() recalcula o conjunto; esse cenário é raro e merece testes específicos.
O guia de SOLID em Python ajuda a avaliar substituição e segregação. A documentação oficial de abc, consultada em 22 de julho de 2026, explica ABC, abstractmethod, registro virtual e detalhes de herança. Prefira o contrato menor que represente uma necessidade real.