Skip to content

Fix: attrs Not Working — Slots Conflict, Validator Errors, and dataclasses Migration

FixDevs ·

Quick Answer

How to fix attrs errors — attrs.define vs attr.s API confusion, __slots__ inheritance issues, validator not running on assignment, converter type narrowing, cattrs structuring failed, and difference from dataclasses.

The Error

You import attrs and the API has changed since old tutorials:

import attr

@attr.s
class User:
    name = attr.ib()
    age = attr.ib()
# Works but emits DeprecationWarning in modern attrs

Or __slots__ inheritance breaks:

import attrs

@attrs.define
class Base:
    name: str

@attrs.define
class Child(Base):
    age: int
    extra_field = "default"   # AttributeError: 'Child' object has no attribute 'extra_field'

Or validators don’t fire on assignment after construction:

import attrs

@attrs.define
class User:
    age: int = attrs.field(validator=attrs.validators.gt(0))

user = User(age=10)
user.age = -5   # No validation — should raise but doesn't

Or converters don’t narrow types as expected:

@attrs.define
class Item:
    price: int = attrs.field(converter=int)

item = Item(price="abc")   # ValueError: invalid literal for int()

Or cattrs serialization fails on a nested attrs class:

import cattrs
data = cattrs.unstructure(some_attrs_obj)
restored = cattrs.structure(data, MyClass)
# Hangs or fails on complex nested types

attrs is the original “write classes with less boilerplate” library — predates the stdlib dataclasses (which it inspired). It’s still actively maintained and offers features dataclasses doesn’t have: validators, converters, more decorator options, optional __slots__. The modern API (@attrs.define) differs from the classic API (@attr.s / @attr.ib()) and most tutorials still use the old syntax. This guide covers the migration and modern usage.

Why This Happens

attrs originally used @attr.s (the “factory” decorator) with attr.ib() (attribute builder) — a unique syntax designed to work on Python 2 (no annotations). Python 3.6+ added type annotations and dataclasses appeared in 3.7. attrs responded with the modern @attrs.define API that uses annotations like dataclasses while keeping attrs’ superior validator/converter system.

The two APIs are otherwise compatible — @attrs.define is the modern surface, @attr.s is the classic surface. Code that mixes both works but is hard to read.

Fix 1: Modern API vs Classic API

# CLASSIC (still works, but old-school)
import attr

@attr.s
class User:
    name = attr.ib()
    age = attr.ib(default=0)

# MODERN (recommended)
import attrs

@attrs.define
class User:
    name: str
    age: int = 0

Modern API key features:

  • @attrs.define — sane defaults (slots=True, weakref_slot=True, init=True, eq=True, hash=False)
  • Type annotations like dataclasses
  • attrs.field() for advanced field config (replaces attr.ib())
  • attrs.frozen for immutable classes
  • attrs.mutable is an alias for attrs.define

Install:

pip install attrs

Field with advanced options:

import attrs
from attrs import field

@attrs.define
class User:
    name: str
    age: int = 0
    email: str = field(validator=attrs.validators.matches_re(r".+@.+"))
    tags: list[str] = field(factory=list)   # Default factory
    _private: int = field(default=0, alias="private")   # Init arg name differs from attr name

Common Mistake: Mixing @attr.s decorator with type annotations expecting them to define fields. The classic decorator ignores annotations:

# WRONG — annotations ignored with @attr.s
@attr.s
class User:
    name: str   # NOT a field
    age = attr.ib()   # IS a field

# CORRECT — either all classic or all modern
@attrs.define
class User:
    name: str
    age: int

Fix 2: Slots and Inheritance

@attrs.define enables __slots__ by default — fast attribute access, lower memory, but no dynamic attributes:

@attrs.define
class User:
    name: str

user = User(name="Alice")
user.email = "[email protected]"   # AttributeError: 'User' object has no attribute 'email'

__slots__ requires every attribute to be declared in advance. If you need dynamic attributes:

@attrs.define(slots=False)
class User:
    name: str

user = User(name="Alice")
user.email = "[email protected]"   # Now works

Inheritance and slots:

@attrs.define
class Base:
    name: str

@attrs.define
class Child(Base):
    age: int

child = Child(name="Alice", age=30)
print(child)   # Child(name='Alice', age=30)

This works correctly — attrs handles slot inheritance internally.

Common Mistake: Adding class-level attributes outside of fields:

@attrs.define
class User:
    name: str
    DEFAULT_ROLE = "user"   # Class-level constant — NOT a field

This works (the constant is on the class, not the instance), but assigning to it on instances fails:

user.DEFAULT_ROLE = "admin"   # AttributeError due to __slots__

Either disable slots or use ClassVar:

from typing import ClassVar

@attrs.define
class User:
    DEFAULT_ROLE: ClassVar[str] = "user"   # Explicitly class-level
    name: str

attrs respects ClassVar — fields with that annotation aren’t instance attributes.

Fix 3: Validators

import attrs
from attrs.validators import gt, lt, ge, le, instance_of, in_, matches_re

@attrs.define
class User:
    name: str = attrs.field(validator=instance_of(str))
    age: int = attrs.field(validator=[gt(0), lt(150)])
    role: str = attrs.field(validator=in_(["admin", "user", "guest"]))
    email: str = attrs.field(validator=matches_re(r".+@.+"))

Custom validators:

def positive_balance(instance, attribute, value):
    if value < 0:
        raise ValueError(f"{attribute.name} must be non-negative")

@attrs.define
class Account:
    balance: float = attrs.field(validator=positive_balance)

The signature is (instance, attribute, value)attribute is the attrs Attribute metadata, useful for the error message.

Validators run only at construction by default:

@attrs.define
class User:
    age: int = attrs.field(validator=attrs.validators.gt(0))

user = User(age=25)   # Validates
user.age = -5         # Does NOT validate — direct assignment bypasses

Enable on-set validation:

@attrs.define(on_setattr=attrs.setters.validate)
class User:
    age: int = attrs.field(validator=attrs.validators.gt(0))

user = User(age=25)
user.age = -5   # ValueError: ...

Pro Tip: Enable on_setattr=attrs.setters.validate on classes you mutate after construction. Pure dataclass-style “init and forget” classes don’t need it (validation happens once at construction). But mutable models — anything you assign to repeatedly during business logic — should validate on every set, not just once. Catches bugs at the actual point of failure.

Combine setters:

@attrs.define(on_setattr=[attrs.setters.convert, attrs.setters.validate])
class Item:
    price: int = attrs.field(converter=int, validator=attrs.validators.gt(0))

Both converters and validators run on every assignment.

Fix 4: Converters

Converters transform input values before assignment:

@attrs.define
class Item:
    price: int = attrs.field(converter=int)
    tags: list[str] = attrs.field(converter=list)
    name: str = attrs.field(converter=str.strip)

item = Item(price="42", tags=("a", "b"), name="  hello  ")
print(item.price)   # 42 (int, not str)
print(item.tags)    # ['a', 'b'] (list, not tuple)
print(item.name)    # 'hello' (stripped)

Optional converter (chain with default):

from attrs import field
from typing import Optional

def parse_int_or_none(value) -> Optional[int]:
    if value is None or value == "":
        return None
    return int(value)

@attrs.define
class Config:
    timeout: Optional[int] = field(default=None, converter=parse_int_or_none)

Converters run AT construction, before validators. If you need conversion on assignment, enable on_setattr=attrs.setters.convert.

Common Mistake: Using a converter that raises and expecting attrs to handle the error nicely. If int("abc") raises ValueError inside a converter, it bubbles up unchanged — attrs doesn’t wrap it. For graceful handling, write a converter that catches and re-raises with context:

def safe_int(value):
    try:
        return int(value)
    except (ValueError, TypeError) as e:
        raise ValueError(f"Cannot convert {value!r} to int") from e

Fix 5: Frozen (Immutable) Classes

@attrs.frozen
class Point:
    x: float
    y: float

p = Point(x=1.0, y=2.0)
p.x = 3.0   # attrs.exceptions.FrozenInstanceError

Frozen classes are immutable — attempting assignment raises. Useful for value objects, configuration, anything that should be hashable.

Frozen classes are automatically hashable:

points = {Point(1, 2), Point(3, 4), Point(1, 2)}
print(len(points))   # 2 (Point(1,2) is deduplicated)

Create modified copies with evolve:

import attrs

p1 = Point(1, 2)
p2 = attrs.evolve(p1, x=10)
print(p1)   # Point(x=1, y=2)
print(p2)   # Point(x=10, y=2)

evolve is the immutable equivalent of mutation — returns a new instance with the specified fields changed.

For pattern-matching style immutable data, frozen attrs classes pair well with structural pattern matching (Python 3.10+):

@attrs.frozen
class Circle:
    radius: float

@attrs.frozen
class Square:
    side: float

def area(shape):
    match shape:
        case Circle(radius=r):
            return 3.14 * r ** 2
        case Square(side=s):
            return s * s

Fix 6: Serialization with cattrs

attrs alone provides classes; cattrs handles serialization to/from dicts:

pip install cattrs
import attrs
import cattrs

@attrs.define
class User:
    name: str
    age: int
    tags: list[str]

user = User(name="Alice", age=30, tags=["admin", "active"])

# To dict
data = cattrs.unstructure(user)
# {'name': 'Alice', 'age': 30, 'tags': ['admin', 'active']}

# From dict
restored = cattrs.structure(data, User)

Nested attrs classes:

@attrs.define
class Address:
    street: str
    city: str

@attrs.define
class Person:
    name: str
    address: Address

person = Person(name="Alice", address=Address(street="123 Main", city="NYC"))
data = cattrs.unstructure(person)
# {'name': 'Alice', 'address': {'street': '123 Main', 'city': 'NYC'}}

restored = cattrs.structure(data, Person)
# Person(name='Alice', address=Address(street='123 Main', city='NYC'))

Custom converters with cattrs:

from datetime import datetime
import cattrs

converter = cattrs.GenConverter()
converter.register_structure_hook(
    datetime,
    lambda value, _: datetime.fromisoformat(value),
)
converter.register_unstructure_hook(
    datetime,
    lambda dt: dt.isoformat(),
)

@attrs.define
class Event:
    when: datetime

event = Event(when=datetime.now())
data = converter.unstructure(event)
# {'when': '2025-04-24T10:00:00.123456'}

restored = converter.structure(data, Event)

Common Mistake: Using stdlib dataclasses.asdict() on attrs classes — works for simple cases but doesn’t handle the full feature set (frozen, slots, validators with custom errors). Use cattrs.unstructure() and cattrs.structure() for consistency across nested structures.

Fix 7: attrs vs dataclasses

# dataclasses (stdlib, Python 3.7+)
from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int = 0

# attrs (third-party)
import attrs

@attrs.define
class User:
    name: str
    age: int = 0

Feature comparison:

Featuredataclassesattrs
In stdlibYesNo
ValidatorsNoYes
ConvertersNoYes
Slots by defaultNo (use slots=True in 3.10+)Yes
FrozenYesYes
__slots__ inheritance handlingManualAutomatic
on_setattr hooksNoYes
Backwards compat to 3.6No (3.7+)Yes
PerformanceSlightly fasterSimilar

When to use which:

  • dataclasses: Simple data containers, no validation needs, prefer stdlib
  • attrs: Need validators/converters/on_setattr, library author requiring older Python, want best-in-class API

For data validation that goes beyond what attrs provides, Pydantic is the heavier alternative — runtime validation with serialization, JSON Schema generation, network type support. attrs is faster and lighter when you don’t need the validation depth.

For Pydantic patterns that overlap with attrs, see Pydantic validation error.

Fix 8: Performance and Memory

attrs with slots is extremely fast — competitive with hand-written __slots__ classes:

@attrs.define   # slots=True by default
class Item:
    name: str
    price: int

Without slots, attrs has overhead:

@attrs.define(slots=False)
class Item:
    name: str
    price: int

Disabling slots adds a __dict__ per instance — bigger memory footprint, slower attribute access. Only disable when you need dynamic attributes.

Hash performance — frozen classes auto-generate __hash__:

@attrs.frozen
class Point:
    x: float
    y: float

# Cached hash is fast for repeated lookups in sets/dicts

Comparison performance — attrs uses tuple comparison internally:

@attrs.define(eq=True, order=True)
class Item:
    priority: int
    name: str

items = [Item(2, "b"), Item(1, "a"), Item(3, "c")]
items.sort()
# Sorted by (priority, name) tuple comparison

Pro Tip: For large object graphs where memory matters, use @attrs.frozen with slots. The combination gives you Python’s fastest object representation: immutable, hashable, fixed-size, no per-instance dict. Reduces memory by 50-80% vs equivalent regular classes for large collections.

Still Not Working?

attrs vs Pydantic vs msgspec

  • attrs — Lightweight class builder with validators. Best for internal classes that need validation but not full serialization.
  • Pydantic — Heavyweight validation + serialization + JSON Schema. Best for API boundaries. See Pydantic validation error.
  • msgspec — Fastest serialization library, similar API to attrs. Best for high-throughput parsing/serialization.

For internal data structures, attrs is the right balance. For external APIs / network boundaries, Pydantic. For performance-critical serialization, msgspec.

Integration with Type Checkers

attrs has dedicated mypy support:

# pyproject.toml
[tool.mypy]
plugins = ["mypy_drf_plugin.main"]   # ...wait, wrong plugin
plugins = []   # attrs has built-in stubs since attrs 21.3+

attrs ships with attrs.pyi stub files — mypy understands @attrs.define natively. For pyright/Pylance, same — full support without configuration.

For mypy-specific patterns with attrs classes, see Python mypy type error.

Builder Pattern

For complex construction with many optional fields, use the builder pattern:

@attrs.define
class HttpRequest:
    url: str
    method: str = "GET"
    headers: dict[str, str] = attrs.field(factory=dict)
    timeout: int = 30
    retries: int = 0

@attrs.define
class HttpRequestBuilder:
    _request: HttpRequest = attrs.field(factory=lambda: HttpRequest(url=""))

    def url(self, value: str) -> "HttpRequestBuilder":
        self._request = attrs.evolve(self._request, url=value)
        return self

    def method(self, value: str) -> "HttpRequestBuilder":
        self._request = attrs.evolve(self._request, method=value)
        return self

    def build(self) -> HttpRequest:
        return self._request

req = HttpRequestBuilder().url("https://api.example.com").method("POST").build()

attrs.evolve makes builders clean for immutable types.

Testing attrs Classes

import attrs
import pytest

@attrs.define
class User:
    name: str = attrs.field(validator=attrs.validators.matches_re(r"\w+"))

def test_user_creation():
    user = User(name="Alice")
    assert user.name == "Alice"

def test_user_validation():
    with pytest.raises(ValueError):
        User(name="invalid!@#")   # Special chars

For pytest fixture patterns with attrs classes, see pytest fixture not found.

Combining with FastAPI

FastAPI uses Pydantic by default but can accept attrs classes via cattrs adapters or by wrapping them in Pydantic models. For pure attrs in FastAPI, you typically convert to a Pydantic model at the API boundary:

import attrs
from pydantic import BaseModel

@attrs.define
class InternalUser:
    name: str
    age: int

class UserResponse(BaseModel):
    name: str
    age: int

@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
    internal = await get_user_from_db(user_id)   # Returns InternalUser
    return UserResponse(name=internal.name, age=internal.age)

For FastAPI dependency patterns, see FastAPI dependency injection error.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles