attrs reduce el código repetitivo de las clases sin ocultar el modelo. Genera inicialización, representación, comparaciones y otros métodos, mientras validadores y conversores expresan las reglas de entrada. Resulta útil cuando una dataclass en Python empieza a acumular comprobaciones manuales.

Crear una clase con attrs

Instala el paquete y usa su API moderna:

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


@define(frozen=True, slots=True)
class Producto:
    nombre: str = field(converter=str.strip)
    precio: Decimal = field(
        converter=Decimal,
        validator=validators.gt(Decimal("0")),
    )


producto = Producto(" Curso Python ", "149.90")
assert producto.nombre == "Curso Python"

define crea la clase, frozen=True impide reasignaciones y slots=True evita un diccionario por instancia. El conversor se ejecuta antes del validador, que recibe un Decimal aunque el llamador entregue una cadena.

Validar relaciones entre campos

Los validadores de campo sirven para reglas locales. Comprueba una invariante que involucra varios atributos después de inicializar:

from attrs import define


@define
class Intervalo:
    inicio: int
    fin: int

    def __attrs_post_init__(self) -> None:
        if self.fin < self.inicio:
            raise ValueError("fin debe ser mayor o igual que inicio")

No uses conversores para corregir silenciosamente entradas ambiguas. Rechaza el estado inválido cuando la corrección cambie el significado. Las anotaciones tampoco validan en ejecución; la guía de type hints en Python explica su función estática.

attrs o dataclasses

Prefiere dataclasses cuando la biblioteca estándar cubra el caso y reducir dependencias sea importante. Considera attrs para validadores combinables, conversores, alias de campos, evolución controlada del constructor o una base de código que ya lo utiliza.

No migres solo por estilo. Compara igualdad, hash, mutabilidad, serialización y firma pública. Las pruebas deben cubrir entradas válidas e inválidas antes de introducir conversores en una clase existente.

La documentación oficial de attrs, consultada el 22 de julio de 2026, detalla clases, campos, validadores, conversores y compatibilidad.

Lista práctica

  • Elige mutabilidad y slots conscientemente.
  • Reserva los conversores para transformaciones inequívocas.
  • Mantén las invariantes cerca del modelo.
  • Devuelve errores claros a quien llama la clase.
  • Prueba el constructor público antes de evolucionarlo.

El valor principal de attrs no es ahorrar caracteres. Es reunir construcción, normalización e invariantes en un contrato comprobable.

Defaults seguros y factories

Los valores mutables no deben compartirse entre instancias. Usa una factory:

from attrs import define, field


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


primero = Pedido(1)
segundo = Pedido(2)
primero.items.append("libro")
assert segundo.items == []

Una factory puede usar takes_self=True, pero queda acoplada al orden de inicialización. Prefiere una propiedad si el valor derivado no necesita almacenarse.

Validadores combinables

attrs.validators ofrece controles de tipo, pertenencia, longitud, opcionalidad y comparación:

from attrs import define, field, validators


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

La comprobación de tipo en ejecución puede proteger límites internos, pero no valida por completo un documento externo. Trata campos ausentes, formatos y errores apropiados para el protocolo antes de construir el modelo. Si None es válido, indícalo en la anotación y con validators.optional(...).

Los validadores propios reciben instancia, atributo y valor. Sus errores deben explicar la regla sin copiar secretos ni todo el payload.

Convertir no es validar

Un conversor normaliza una representación inequívoca. Limpiar texto, construir un enum o convertir a Decimal son usos razonables. No conviertas entradas arbitrarias con bool: bool("false") produce True. Tampoco redondees dinero ni repares identificadores silenciosamente.

La conversión ocurre antes de validar. Prueba ese pipeline si la clase es pública. Cambiar un conversor puede modificar igualdad, hash y serialización aunque la anotación siga igual.

Alias y evolución de la API

Un atributo _token suele aparecer como token en el inicializador generado. La API moderna permite controlar el parámetro con alias, pero eso no mantiene automáticamente ambos nombres.

Prefiere argumentos con nombre y campos keyword-only:

from attrs import define, field


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

Agregar un campo opcional keyword-only es más seguro que reordenar parámetros posicionales. En bibliotecas, conviene probar la firma pública.

Objetos congelados, slots y hash

frozen=True impide la asignación normal, pero no vuelve inmutable una lista contenida. Usa tuplas y frozenset cuando todo el valor deba permanecer estable. Un objeto usado como clave no puede cambiar los campos que participan en su hash.

slots=True evita atributos accidentales y puede reducir memoria. Antes de activarlo en una clase existente, prueba herencia, introspección, referencias débiles, pickle y herramientas que esperan __dict__. Decide con necesidades y mediciones reales.

Serialización explícita

attrs.asdict() produce diccionarios de forma recursiva. Puede copiar un grafo grande y revelar campos internos:

from attrs import asdict, filters

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

No lo consideres un contrato JSON automático. Fechas, decimales, enums y valores propios necesitan una política. Para APIs duraderas, crea un DTO explícito o filtros y transformaciones probados. Nunca expongas credenciales solo por ser atributos.

attrs.evolve(objeto, campo=valor) crea una copia modificada y vuelve a ejecutar inicialización, conversores y validadores. Es útil con modelos congelados, aunque debes comprobar campos derivados y init=False.

Herencia o composición

La herencia introduce restricciones entre atributos obligatorios y atributos con default. Los campos keyword-only resuelven algunos casos, pero la composición suele representar mejor el dominio. Un pedido puede contener una dirección y una política de precios sin heredar de ellas.

Si necesitas herencia, prueba el constructor final y la cadena de __attrs_post_init__. Consulta la documentación de la versión instalada antes de combinar clases attrs y bases comunes.

Pruebas del contrato

  • construcción válida con argumentos nombrados;
  • rechazo de cada invariante;
  • conversión antes de validación;
  • factories mutables independientes;
  • igualdad, orden y hash cuando son públicos;
  • representación sin datos sensibles;
  • evolución de instancias congeladas;
  • serialización limitada a campos públicos;
  • comprobación estática separada de la validación en ejecución.

Comprueba comportamiento y no el repr completo, salvo que sea deliberadamente público. Así las actualizaciones son menos frágiles.

Migración gradual

Antes de reemplazar una clase manual, registra firma, defaults, igualdad y errores. Convierte una clase por vez y conserva pruebas de caracterización. El código nuevo debería preferir define, field y frozen.

Evita mezclar varias bibliotecas de modelado en un mismo límite sin una razón concreta. attrs modela objetos Python; validar documentos externos, persistir con un ORM y generar schemas son responsabilidades diferentes.

Consideraciones operativas

Mantén validadores deterministas y sin acceso a red o base de datos. Construir un objeto no debería iniciar I/O inesperado. Resuelve hechos externos en un servicio y pasa el resultado al modelo. Así los errores son predecibles y la construcción funciona igual en pruebas, scripts y workers.

El repr generado ayuda a depurar, pero puede incluir información personal. Usa repr=False en campos sensibles y, además, sanea los logs en su límite. Del mismo modo, aplica eq=False a metadatos operativos solo después de decidir si dos objetos con metadatos diferentes deben considerarse iguales.

Si el tipado estático es importante, ejecuta el verificador del proyecto sobre constructores representativos. attrs expone metadatos que entienden las herramientas modernas, pero el comportamiento real depende de versiones y opciones instaladas. Revisa upgrades, consulta sus cambios y prueba firmas públicas antes de publicar.

Un buen modelo rechaza estados inválidos sin apropiarse de todas las responsabilidades. Una clase attrs pequeña y explícita resulta más mantenible que otra que analiza documentos de transporte, consulta almacenamiento, serializa respuestas y aplica toda la política de dominio.