attrs generates Python class boilerplate from field annotations. pip install attrs. Modern API: from attrs import define, field, Factory. @define class User: name: str; email: str; role: str = "user". Auto-generates __init__, __repr__, __eq__, __hash__. Field: age: int = field(default=0). Validator: name: str = field(validator=attrs.validators.instance_of(str)). Multiple validators: field(validator=[instance_of(str), min_len]). Custom validator: @name.validator def check_name(self, attribute, value): if not value: raise ValueError("name required"). Converter: email: str = field(converter=str.lower). age: int = field(converter=int). Factory: tags: list = field(factory=list). items: list = field(default=Factory(list)). Frozen (immutable): @define(frozen=True) class Point: x: float; y: float. p = Point(1,2); p2 = attrs.evolve(p, x=3) — new instance. Slots: @define(slots=True) — faster attribute access, smaller memory. asdict(obj) — convert to dict. astuple(obj) — convert to tuple. attrs.has(cls) — check if class uses attrs. attrs.fields(cls) — list of Attribute objects. attrs.fields(User).name.name. make_class("Point", ["x","y"]) — dynamic. On __attrs_post_init__: def __attrs_post_init__(self): self._derived = self.x + self.y. Aliases: from attr import attrs as attrs_dec, attrib — older API. @attr.s(auto_attribs=True) — enable type-annotation-based field discovery. eq=False — disable equality (useful for mutable objects). order=True — enable <>/<=/>= comparison. hash=None — auto from eq/frozen. repr=False — disable repr for sensitive fields. Validators module: attrs.validators.and_(v1,v2), not_(v), optional(v), deep_iterable(member_validator), deep_mapping(key_validator, value_validator), in_(allowed_values), lt(n), le(n), gt(n), ge(n). Claude Code generates attrs domain models, validators, converters, and frozen value objects.
CLAUDE.md for attrs
## attrs Stack
- Version: attrs >= 23.2 | pip install attrs
- Define: @define class C: field: type = attrs.field(validator=..., converter=...)
- Validators: instance_of | in_ | lt/le/gt/ge | and_ | optional | deep_iterable
- Converter: field(converter=str.lower) — auto-cast on assignment/__init__
- Mutable default: field(factory=list) — never field(default=[]) footgun
- Frozen: @define(frozen=True) + attrs.evolve(obj, field=new_val) for updates
- Serialize: attrs.asdict(obj) | attrs.astuple(obj)
attrs Class Generation Pipeline
# app/domain.py — attrs domain models
from __future__ import annotations
import re
import uuid
from datetime import datetime, timezone
from decimal import Decimal
from enum import Enum
from typing import Optional
import attrs
from attrs import Factory, define, field
from attrs import validators as v
# ─────────────────────────────────────────────────────────────────────────────
# Custom validators
# ─────────────────────────────────────────────────────────────────────────────
def validate_email(instance, attribute, value: str) -> None:
pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
if not re.match(pattern, value):
raise ValueError(f"{attribute.name!r} must be a valid email, got {value!r}")
def validate_non_empty(instance, attribute, value: str) -> None:
if not value.strip():
raise ValueError(f"{attribute.name!r} must not be empty or whitespace")
def validate_sku(instance, attribute, value: str) -> None:
if not re.match(r"^[A-Z0-9]+-\d{4,}$", value):
raise ValueError(f"SKU must match PROD-NNNN format, got {value!r}")
# ─────────────────────────────────────────────────────────────────────────────
# Enums
# ─────────────────────────────────────────────────────────────────────────────
class UserRole(str, Enum):
USER = "user"
MODERATOR = "moderator"
ADMIN = "admin"
class OrderStatus(str, Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
# ─────────────────────────────────────────────────────────────────────────────
# Value objects — frozen (immutable)
# ─────────────────────────────────────────────────────────────────────────────
@define(frozen=True)
class Money:
"""Immutable monetary amount — supports arithmetic via evolve()."""
amount: Decimal
currency: str = field(default="USD", converter=str.upper)
def __attrs_post_init__(self) -> None:
if self.amount < 0:
raise ValueError(f"Money.amount cannot be negative: {self.amount}")
def add(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError(f"Currency mismatch: {self.currency} vs {other.currency}")
return attrs.evolve(self, amount=self.amount + other.amount)
def multiply(self, factor: int | Decimal) -> "Money":
return attrs.evolve(self, amount=self.amount * Decimal(str(factor)))
def __str__(self) -> str:
return f"{self.currency} {self.amount:.2f}"
@define(frozen=True, order=True)
class Address:
"""Ordered frozen value object — supports sorting by (state, city, street)."""
street: str = field(validator=[v.instance_of(str), validate_non_empty])
city: str = field(validator=[v.instance_of(str), validate_non_empty])
state: str = field(converter=str.upper,
validator=v.matches_re(r"^[A-Z]{2}$"))
postal_code: str = field(validator=v.matches_re(r"^\d{5}(-\d{4})?$"))
country: str = field(default="US", converter=str.upper)
# ─────────────────────────────────────────────────────────────────────────────
# Mutable entities
# ─────────────────────────────────────────────────────────────────────────────
@define
class User:
"""Mutable user entity — validated on construction, eq based on id."""
id: str = field(factory=lambda: str(uuid.uuid4()))
email: str = field(validator=[v.instance_of(str), validate_email],
converter=str.lower)
first_name: str = field(validator=[v.instance_of(str), validate_non_empty])
last_name: str = field(validator=[v.instance_of(str), validate_non_empty])
role: UserRole = field(
default=UserRole.USER,
validator=v.in_(list(UserRole)),
converter=lambda r: UserRole(r) if isinstance(r, str) else r,
)
is_active: bool = field(default=True, validator=v.instance_of(bool))
tags: list[str] = field(factory=list,
validator=v.deep_iterable(v.instance_of(str)))
address: Optional[Address] = field(
default=None,
validator=v.optional(v.instance_of(Address)),
)
created_at: datetime = field(
factory=lambda: datetime.now(timezone.utc),
validator=v.instance_of(datetime),
)
def __attrs_post_init__(self) -> None:
# Cross-field validation after all fields are set
if not self.first_name[0].isupper():
raise ValueError(f"first_name should start with uppercase: {self.first_name!r}")
@property
def full_name(self) -> str:
return f"{self.first_name} {self.last_name}"
def promote_to_admin(self) -> None:
self.role = UserRole.ADMIN
@define
class Product:
sku: str = field(validator=[v.instance_of(str), validate_sku])
name: str = field(validator=[v.instance_of(str), validate_non_empty])
price: Money
stock: int = field(default=0, validator=[v.instance_of(int), v.ge(0)])
category: str = field(validator=v.in_(["Electronics","Clothing","Books","Home","Sports"]))
is_active: bool = True
id: str = field(factory=lambda: str(uuid.uuid4()))
tags: list[str] = field(factory=list)
def reserve(self, quantity: int) -> None:
if quantity > self.stock:
raise ValueError(f"Cannot reserve {quantity} — only {self.stock} in stock")
self.stock -= quantity
def restock(self, quantity: int) -> None:
if quantity < 1:
raise ValueError(f"Restock quantity must be positive: {quantity}")
self.stock += quantity
@define(eq=False) # entity equality uses id, not field values
class OrderLine:
product_id: str
sku: str
quantity: int = field(validator=[v.instance_of(int), v.ge(1)])
unit_price: Money
@property
def subtotal(self) -> Money:
return self.unit_price.multiply(self.quantity)
@define(eq=False)
class Order:
id: str = field(factory=lambda: str(uuid.uuid4()))
user_id: str
lines: list[OrderLine] = field(factory=list)
status: OrderStatus = OrderStatus.PENDING
notes: Optional[str] = None
created_at: datetime = field(factory=lambda: datetime.now(timezone.utc))
@property
def total(self) -> Money:
if not self.lines:
return Money(Decimal("0"))
total = self.lines[0].subtotal
for line in self.lines[1:]:
total = total.add(line.subtotal)
return total
def add_line(self, line: OrderLine) -> None:
self.lines.append(line)
def cancel(self) -> None:
if self.status in (OrderStatus.SHIPPED, OrderStatus.DELIVERED):
raise ValueError("Cannot cancel a shipped or delivered order")
self.status = OrderStatus.CANCELLED
# ─────────────────────────────────────────────────────────────────────────────
# Serialization with asdict / astuple
# ─────────────────────────────────────────────────────────────────────────────
def user_to_dict(user: User) -> dict:
"""Convert User to a JSON-serializable dict."""
d = attrs.asdict(user)
d["role"] = user.role.value
d["created_at"] = user.created_at.isoformat()
if user.address:
d["address"] = attrs.asdict(user.address)
return d
if __name__ == "__main__":
# Create user — validators run automatically
alice = User(
email="[email protected]", # converter lowercases to [email protected]
first_name="Alice",
last_name="Smith",
role="admin", # converter converts str → UserRole.ADMIN
address=Address(
street="123 Main St",
city="Springfield",
state="il", # converter uppercases to IL
postal_code="62701",
),
)
print(f"User: {alice.full_name} <{alice.email}> role={alice.role.value}")
print(f"Address: {alice.address.city}, {alice.address.state}")
# Frozen Money value object
price = Money(Decimal("19.99"))
tax = Money(Decimal("1.60"))
total = price.add(tax)
print(f"\nPrice: {price}")
print(f"Total with tax: {total}")
# Evolve — new instance, original unchanged
doubled = price.multiply(2)
print(f"Doubled: {doubled}")
# Order
order = Order(user_id=alice.id)
line = OrderLine(
product_id="prod-001",
sku="BOOK-1001",
quantity=3,
unit_price=Money(Decimal("9.99")),
)
order.add_line(line)
print(f"\nOrder total: {order.total}")
# Serialize
data = user_to_dict(alice)
print(f"\nSerialized keys: {list(data.keys())}")
For the dataclasses alternative — Python dataclasses generate __init__, __repr__, and __eq__ but have no validator or converter hooks: @dataclass class User: email: str accepts any string without validation, and field(default_factory=list) is the only way to avoid the mutable-default footgun, while attrs’s field(converter=str.lower, validator=[instance_of(str), validate_email]) enforces the email format on construction and normalises case in one line. For the Pydantic BaseModel alternative — Pydantic is primarily a parsing and serialization library where instances are semi-frozen after creation, while attrs@define(eq=False) creates a fully mutable entity whose identity is its id field (not field equality), attrs.evolve(frozen_obj, x=new_val) creates a modified copy of a frozen value object without mutating the original, and @define(slots=True) uses __slots__ to reduce memory usage for high-volume domain objects by 20–40%. The Claude Skills 360 bundle includes attrs skill sets covering @define vs @attrs/attr.s, field validators and converters, Factory for mutable defaults, frozen value objects with evolve, slots optimization, attrs_post_init for cross-field validation, asdict/astuple serialization, deep_iterable and optional validators, make_class for dynamic classes, and eq=False for identity-based entities. Start with the free tier to try domain modeling code generation.