attrs/docs/types.md

130 lines
5.6 KiB
Markdown
Raw Normal View History

2022-12-20 06:49:00 +00:00
# Type Annotations
2023-05-22 05:46:02 +00:00
*attrs* comes with first-class support for type annotations for both {pep}`526` and legacy syntax.
2022-12-20 06:49:00 +00:00
2024-07-15 09:27:08 +00:00
However, they will remain *optional* forever, therefore the example from the README could also be written as:
2022-12-20 06:49:00 +00:00
```{doctest}
>>> from attrs import define, field
2022-12-20 06:49:00 +00:00
>>> @define
... class SomeClass:
... a_number = field(default=42)
... list_of_numbers = field(factory=list)
2022-12-20 06:49:00 +00:00
>>> sc = SomeClass(1, [1, 2, 3])
>>> sc
SomeClass(a_number=1, list_of_numbers=[1, 2, 3])
2022-12-20 06:49:00 +00:00
```
You can choose freely between the approaches, but please remember that if you choose to use type annotations, you **must** annotate **all** attributes!
:::{caution}
If you define a class with a {func}`attrs.field` that **lacks** a type annotation, *attrs* will **ignore** other fields that have a type annotation, but are not defined using {func}`attrs.field`:
```{doctest}
>>> @define
... class SomeClass:
... a_number = field(default=42)
... another_number: int = 23
>>> SomeClass()
SomeClass(a_number=42)
```
:::
2022-12-20 06:49:00 +00:00
2024-07-15 09:27:08 +00:00
Even when going all-in on type annotations, you will need {func}`attrs.field` for some advanced features, though.
2022-12-20 06:49:00 +00:00
One of those features are the decorator-based features like defaults.
It's important to remember that *attrs* doesn't do any magic behind your back.
All the decorators are implemented using an object that is returned by the call to {func}`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.
---
Please note that 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!
Because Python does not allow references to a class object before the class is defined,
types may be defined as string literals, so-called *forward references* ({pep}`526`).
2024-07-15 09:27:08 +00:00
You can enable this automatically for a whole module by using `from __future__ import annotations` ({pep}`563`).
2022-12-20 06:49:00 +00:00
In this case *attrs* simply puts these string literals into the `type` attributes.
If you need to resolve these to real types, you can call {func}`attrs.resolve_types` which will update the attribute in place.
2024-07-15 09:27:08 +00:00
In practice though, types show their biggest usefulness in combination with tools like [Mypy], [*pytype*], or [Pyright] that have dedicated support for *attrs* classes.
2022-12-20 06:49:00 +00:00
The addition of static types is certainly one of the most exciting features in the Python ecosystem and helps you write *correct* and *verified self-documenting* code.
## Mypy
2024-07-15 09:27:08 +00:00
While having a nice syntax for type metadata is great, it's even greater that [Mypy] ships with a dedicated *attrs* plugin which allows you to statically check your code.
2022-12-20 06:49:00 +00:00
Imagine you add another line that tries to instantiate the defined class using `SomeClass("23")`.
Mypy will catch that error for you:
```console
$ mypy t.py
t.py:12: error: Argument 1 to "SomeClass" has incompatible type "str"; expected "int"
```
This happens *without* running your code!
2024-07-15 09:27:08 +00:00
And it also works with *both* legacy annotation styles.
To Mypy, this code is equivalent to the one above:
2022-12-20 06:49:00 +00:00
```python
@attr.s
class SomeClass:
a_number = attr.ib(default=42) # type: int
list_of_numbers = attr.ib(factory=list, type=list[int])
```
2024-07-15 09:27:08 +00:00
The approach used for `list_of_numbers` one is only a available in our [old-style API](names.md) which is why the example still uses it.
2022-12-20 06:49:00 +00:00
## Pyright
2024-07-15 09:27:08 +00:00
*attrs* provides support for [Pyright] through the `dataclass_transform` / {pep}`681` specification.
2022-12-20 06:49:00 +00:00
This provides static type inference for a subset of *attrs* equivalent to standard-library {mod}`dataclasses`,
and requires explicit type annotations using the {func}`attrs.define` or `@attr.s(auto_attribs=True)` API.
2024-07-15 09:27:08 +00:00
Given the following definition, Pyright will generate static type signatures for `SomeClass` attribute access, `__init__`, `__eq__`, and comparison methods:
2022-12-20 06:49:00 +00:00
```
2024-07-15 09:27:08 +00:00
@attrs.define
2022-12-20 06:49:00 +00:00
class SomeClass:
a_number: int = 42
list_of_numbers: list[int] = attr.field(factory=list)
```
:::{warning}
2024-07-15 09:27:08 +00:00
The Pyright inferred types are a tiny subset of those supported by Mypy, including:
2022-12-20 06:49:00 +00:00
- The `attrs.frozen` decorator is not typed with frozen attributes, which are properly typed via `attrs.define(frozen=True)`.
Your constructive feedback is welcome in both [attrs#795](https://github.com/python-attrs/attrs/issues/795) and [pyright#1782](https://github.com/microsoft/pyright/discussions/1782).
2024-07-15 09:27:08 +00:00
Generally speaking, 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, though.
2022-12-20 06:49:00 +00:00
:::
## Class variables and constants
If you are adding type annotations to all of your code, you might wonder how to define a class variable (as opposed to an instance variable), because a value assigned at class scope becomes a default for that attribute.
The proper way to type such a class variable, though, is with {data}`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 {data}`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
...
```
2024-07-15 09:27:08 +00:00
[Mypy]: http://mypy-lang.org
[Pyright]: https://github.com/microsoft/pyright
2022-12-20 06:49:00 +00:00
[*pytype*]: https://google.github.io/pytype/