Type Annotations

The addition of static types is one of the most exciting features in the Python ecosystem and helps you write correct and verified self-documenting code. attrs comes with first-class support for type annotations for both PEP 526 and legacy syntax.

However, they will remain optional forever, therefore the example from the README could also be written as:

>>> from attrs import define, field

>>> @define
... class SomeClass:
...     a_number = field(default=42)
...     list_of_numbers = field(factory=list)

>>> sc = SomeClass(1, [1, 2, 3])
>>> sc
SomeClass(a_number=1, list_of_numbers=[1, 2, 3])

You can choose freely between the approaches, but remember that if you choose to use type annotations, you must annotate all attributes!

Caution

If you define a class with a attrs.field() that lacks a type annotation, attrs will ignore other fields that have a type annotation, but are not defined using attrs.field():

>>> @define
... class SomeClass:
...     a_number = field(default=42)
...     another_number: int = 23
>>> SomeClass()
SomeClass(a_number=42)

Even when going all-in on type annotations, you will need attrs.field() for some advanced features, though.

One of those features is decorator-based functionality like defaults. It’s important to remember that attrs does not perform any hidden magic here: The decorators are implemented using the object returned by the call to attrs.field(). Attributes that only carry a class annotation do not have that object, so trying to call a method on it will inevitably fail.

Forward references

Python doesn’t allow referencing classes in type annotations that haven’t been defined yet. Since this is a common requirement in real-world code, the traditional workaround has been defining them using string literals:

class C:
    another_c: "C"

These are called forward references (PEP 526) and can be enabled automatically for a whole file by using from __future__ import annotations (PEP 563).

As of Python 3.14, this is no longer necessary because it introduced deferred evaluation of annotations (PEP 649 and PEP 749) that has a more sophisticated system based on annotationlib.ForwardRefs, but ultimately solves the same problem.

In both cases, if you need to resolve these to real types, you can call attrs.resolve_types(), which will update the attributes in place.

Class variables and constants

Defining an attribute with a type annotation and assigned value looks like a class variable, but it isn’t. It’s an instance variable with a default value.

The correct way to define a class variable is with typing.ClassVar, which indicates that the variable should only be assigned in the class (or its subclasses) and not in instances of the class. attrs will skip over members annotated with typing.ClassVar, allowing you to write a type annotation without turning the member into an attribute. Class variables are often used for constants, though they can also be used for mutable singleton data shared across all instances of the class.

@attrs.define
class PngHeader:
    SIGNATURE: typing.ClassVar[bytes] = b'\x89PNG\r\n\x1a\n'
    height: int
    width: int
    interlaced: int = 0
    ...

Overview of type checkers

Types – regardless how added – are only metadata that can be queried from the class and they aren’t used for anything out of the box. Some packages like cattrs or Pydantic use this metadata for runtime type validation and (de-)serialization.

But their original purpose is to support static type-checking tools and IDEs.

Over the years, the type-checking community has come up with PEP 681 that defines dataclass_transform to offer a baseline way to define dataclass-like class generators. All modern type-checking implementations support that, but in practice it’s not enough to make the most out of attrs, so some offer further attrs-specific support.

Mypy

Mypy is the original Python type checker and ships with a dedicated attrs plugin that implements our features far beyond PEP 681.

It also works with both legacy annotation styles. With Mypy, you can also write this (but really shouldn’t):

@attr.s
class SomeClass:
    a_number = attr.ib(default=42)  # type: int
    list_of_numbers = attr.ib(factory=list, type=list[int])

The approach used for list_of_numbers is only available in our old-style API which is why the example still uses it.

Pyright / VS Code

attrs integrates with Microsoft’s Pyright via PEP 681. While Pyright is not as commonly used as a standalone type checker, it’s widely used as the foundation of VS Code’s proprietary Pylance language server that powers its Python support.

Pyright has grown several attrs-specific features over the years, but its inferred types are still a tiny subset of those supported by Mypy, including:

  • Auto-aliasing of private attributes is not supported. You have to manually tell Pyright about it: _x: int = attrs.field(alias="x")

  • None of the decorator-based features (like @_x.default or @_x.validator) are supported.

Your constructive feedback is welcome in both attrs#795 and pyright#1782. Keep in mind that the decision on improving attrs support in Pyright is entirely Microsoft’s prerogative and they unequivocally indicated that they’ll only add support for features that go through the PEP process. We as the attrs project unfortunately have no influence over that.

Note that there’s a community fork called basedpyright that implements some of Microsoft’s closed-source Pylance features, so they’re available in other editors like Zed and VS Code forks that are not allowed to use Pylance. Unfortunately, better attrs support doesn’t appear to be part of their goals.

ty

ty is a fairly new Rust-based type checker from Astral, the makers of Ruff and uv. Currently it only supports PEP 681, but they intend to support more of attrs’s features.

Pyrefly

Pyrefly is Meta’s take on a Rust-based type checker for Python. It also only implements PEP 681 and based on the (lack of) activity on attrs-related issues on their bug tracker there is currently no indication that they plan to support additional attrs features.