attrs reduz o código repetitivo de classes sem esconder o modelo. A biblioteca gera inicialização, representação, comparação e outros métodos, enquanto validadores e conversores tornam as regras de entrada explícitas. Ela é útil quando uma dataclass em Python começa a acumular verificações manuais.

Criar uma classe com attrs

Instale attrs no ambiente do projeto e use a API moderna:

python -m pip install attrs
from decimal import Decimal
from attrs import define, field, validators


@define(frozen=True, slots=True)
class Produto:
    nome: str = field(converter=str.strip)
    preco: Decimal = field(
        converter=Decimal,
        validator=validators.gt(Decimal("0")),
    )


produto = Produto(" Curso Python ", "149.90")
assert produto.nome == "Curso Python"

define cria a classe, frozen=True impede reatribuição e slots=True evita um dicionário por instância. O conversor roda antes do validador. Portanto, o validador recebe Decimal, mesmo quando o chamador envia uma string.

Validar relações entre campos

Validadores de campo são adequados para regras locais. Para uma invariante que depende de vários atributos, valide após a inicialização:

from attrs import define


@define
class Intervalo:
    inicio: int
    fim: int

    def __attrs_post_init__(self) -> None:
        if self.fim < self.inicio:
            raise ValueError("fim deve ser maior ou igual ao início")

Não use conversores para corrigir silenciosamente dados ambíguos. Uma API deve rejeitar estados inválidos quando a correção mudar o significado da entrada. Também não confunda anotações com validação: o guia de type hints em Python explica o papel da análise estática.

attrs ou dataclasses

Prefira dataclasses quando a biblioteca padrão já cobre o caso e reduzir dependências é importante. Considere attrs quando você precisa de validadores combináveis, conversores, aliases de campos, evolução controlada do construtor ou compatibilidade com uma base de código que já adotou a biblioteca.

Evite migrar apenas por estilo. Compare comportamento de igualdade, hash, mutabilidade, serialização e assinatura pública. Testes devem cobrir entradas válidas e inválidas, especialmente se conversores forem adicionados a uma classe existente.

A documentação oficial do attrs, consultada em 22 de julho de 2026, detalha define, campos, validadores, conversores e compatibilidade. Use-a como referência para a versão instalada no projeto.

Checklist prático

  • Escolha mutabilidade e slots de forma consciente.
  • Use conversores apenas para transformações inequívocas.
  • Valide invariantes perto do modelo.
  • Mantenha erros claros para quem chama a classe.
  • Cubra a assinatura pública com testes antes de evoluí-la.

O ganho principal do attrs não é escrever menos caracteres. É manter construção, normalização e invariantes no mesmo contrato, sem espalhar verificações por serviços e controllers.

Defaults seguros e factories

Valores mutáveis não devem ser compartilhados entre instâncias. Use Factory ou factory=list em vez de uma lista criada na declaração:

from attrs import define, field


@define
class Pedido:
    cliente_id: int
    itens: list[str] = field(factory=list)


primeiro = Pedido(1)
segundo = Pedido(2)
primeiro.itens.append("livro")
assert segundo.itens == []

Uma factory também pode depender da própria instância com takes_self=True, mas isso aumenta o acoplamento à ordem de inicialização. Prefira calcular valores derivados em uma propriedade quando eles não precisam ser armazenados.

Validadores combináveis

attrs.validators inclui verificações de tipo, pertencimento, comprimento, opcionalidade e comparações. Validadores podem ser combinados sem esconder qual regra falhou:

from attrs import define, field, validators


@define
class Usuario:
    nome: str = field(
        converter=str.strip,
        validator=[
            validators.instance_of(str),
            validators.min_len(2),
        ],
    )
    nivel: str = field(validator=validators.in_({"basico", "admin"}))

instance_of é validação em execução e pode ser útil em limites internos, mas não substitui a validação completa de um payload externo. Antes de construir o modelo, trate campos ausentes, formatos e mensagens adequadas ao protocolo. Se um campo aceita None, declare essa decisão com validators.optional(...) e no type hint.

Para regras específicas, escreva uma função com a assinatura (instancia, atributo, valor) ou use @campo.validator. Produza mensagens que identifiquem a regra, mas não inclua segredos ou o payload inteiro.

Conversão não é validação

Um conversor normaliza uma representação inequívoca. str.strip, uma enumeração ou Decimal são usos comuns. Não converta silenciosamente qualquer valor com bool: bool("false") resulta em True. Também evite arredondar preços ou corrigir identificadores dentro do modelo sem uma regra explícita.

O pipeline ocorre durante a inicialização: alias e argumentos alimentam os campos, conversores transformam valores e validadores verificam o resultado. Teste esse contrato, principalmente quando a classe é pública. Uma mudança de conversor pode alterar igualdade, hash e serialização mesmo sem mudar a anotação.

Campos privados, aliases e evolução da API

Um atributo chamado _token normalmente aparece como token no construtor gerado. A API moderna permite declarar alias quando você precisa controlar esse nome. Isso é útil em migrações, mas não cria compatibilidade automática com o nome antigo.

Inspecione a assinatura gerada em testes de API e prefira argumentos nomeados. Ao adicionar um campo opcional, coloque-o depois dos obrigatórios ou use campos keyword-only. kw_only=True reduz ambiguidades e permite evoluir o construtor sem reordenar argumentos posicionais.

from attrs import define, field


@define
class Configuracao:
    endpoint: str
    timeout: float = field(default=5.0, kw_only=True)

Imutabilidade, slots e hash

frozen=True bloqueia a atribuição normal, mas não torna imutável o conteúdo de uma lista armazenada. Para um objeto realmente estável, use também tipos imutáveis, como tuplas e frozenset. Imutabilidade ajuda no raciocínio e pode permitir hash, desde que todos os campos relevantes também sejam hashable.

slots=True reduz atributos acidentais e pode economizar memória em grandes quantidades de instâncias. Antes de ativá-lo em uma classe existente, teste herança, introspecção, weak references e ferramentas que esperam __dict__. Não escolha slots apenas com base em microbenchmark.

Deixe attrs decidir eq e hash pelos parâmetros de classe, salvo quando a identidade do domínio exigir outra regra. Objetos usados como chaves de dicionário não podem mudar os campos que participam do hash.

Serialização explícita

attrs.asdict() cria estruturas recursivas por padrão. Isso é conveniente, mas pode copiar muito conteúdo e expor campos internos:

from attrs import asdict, filters

publico = asdict(
    usuario,
    filter=filters.exclude("token"),
)

Não trate asdict() como contrato JSON automático. Datas, decimais, enums e objetos próprios ainda precisam de uma política de codificação. Para APIs duradouras, monte um DTO explícito ou use filtros e transformações testados. Nunca serialize senhas, tokens ou campos operacionais apenas porque estão no objeto.

attrs.evolve(objeto, campo=valor) cria uma cópia modificada passando novamente pelo inicializador, conversores e validadores. É uma opção clara para modelos congelados. Confirme, porém, como campos com init=False e valores derivados se comportam.

Herança e composição

Herança de classes de dados traz restrições de ordenação entre campos obrigatórios e campos com default. Campos keyword-only podem resolver parte do problema, mas composição costuma representar melhor um domínio. Em vez de uma árvore profunda, faça um modelo conter endereço, preço ou política.

Se herança for necessária, teste a assinatura final e o encadeamento de __attrs_post_init__. attrs não chama automaticamente toda lógica de inicialização escrita como se fosse uma classe Python manual. Consulte a documentação da versão instalada antes de misturar classes attrs e classes comuns.

Testes que protegem o contrato

  • construção com valores válidos e argumentos nomeados;
  • rejeição de cada invariante com o tipo de erro esperado;
  • ordem entre conversão e validação;
  • independência de factories mutáveis;
  • igualdade, ordenação e hash quando fazem parte da API;
  • representação sem dados sensíveis;
  • evolução de objetos congelados;
  • saída serializada com apenas os campos públicos;
  • type checking no CI, separado da validação em execução.

Teste comportamento, não o texto inteiro do repr gerado, a menos que ele seja deliberadamente público. Isso reduz mudanças frágeis ao atualizar attrs.

Migração gradual

Para migrar uma classe manual, registre primeiro sua assinatura, igualdade, defaults e erros. Converta uma classe por vez e mantenha testes de caracterização. attrs também oferece APIs compatíveis com estilos mais antigos, mas projetos novos devem preferir define, field e frozen.

Evite misturar várias bibliotecas de modelagem no mesmo limite sem uma razão clara. attrs modela objetos Python; validação de documentos externos, ORM e geração de schema são responsabilidades diferentes. A escolha correta depende do contrato necessário, não da quantidade de linhas economizadas.