The package Python Attrs allows you to create advanced data classes using simple annotations. Of course, python has its own native data class module as well, but the Python attrs package offers a couple of extra features you might like!
Install attrs
The attrs package is not part of the base library, so you will need to install it with the pip install command or something similar, like uv. You probably also want to create a virtual environment, so it won’t interfere with other projects you have. The package is called attrs, so installing it will look like this:
$ pip install attrs # or, in a uv project: $ uv add attrs
The examples in this article use the modern attrs API, which you import from the attrs module: from attrs import define, field. You’ll still see older code that uses import attr with @attr.s and attr.ib(). That API still works, but for new code, the attrs authors recommend @define and field().
Attrs vs data classes
The authors of attrs have, in fact, worked on the PEP that introduced data classes into Python. Python’s native data classes are intentionally kept simpler and easier to understand while attrs offers the full range of features you might want!
Data classes have caught up in some areas. Since Python 3.10, they support slots=True and keyword-only fields with kw_only=True. Still, some of the reasons to choose Python attrs over the built-in data classes are:
- You want more features: attrs offers validators and converters, which data classes don’t have.
- You want good defaults: classes made with
@defineare slotted by default, for less memory usage and faster attribute access, and they run your validators when you change an attribute too. - You want new features without waiting for a new Python release. attrs is released independently of Python, so you get improvements on whatever Python version you use.
If you don’t need any of this, the built-in data classes are a fine choice, with no extra dependency.
A basic Python attrs example
Let’s look at a very basic example first:
from attrs import define, field
@define
class Person:
name: str = 'John'
surname: str = 'Doe'
age: int | None = field(default=None, init=False)
p = Person()
print(p)
p = Person('Bill', 'Gates')
p.age = 60
print(p)
# Output:
# Person(name='John', surname='Doe', age=None)
# Person(name='Bill', surname='Gates', age=60)Code language: Python (python)
A couple of observations:
- The syntax looks almost the same as that of data classes: you declare the attributes with type annotations and optional defaults.
- Similar to data classes, you get a nicely formatted representation of your data when you print it.
- When you need more than a default value, use
field(). Here,init=Falseleavesageout of the generated__init__method, so you can’t pass it when creating aPerson.
Next let’s look at the most important features this package offers over regular data classes: validators and converters.
Python attrs validator example
You can add validators to your attrs data class in two ways:
- Using a decorator
- By providing a callable function
I’ll demonstrate the callable function method here first. Attrs offers several validators out of the box, of which we’ll use the instance_of validator in the following example:
from attrs import define, field, validators
@define
class C:
x: int = field(validator=validators.instance_of(int))
print(C(42))
C("a string")
# Output:
# C(x=42)
# Traceback (most recent call last):
# ...
# TypeError: ("'x' must be <class 'int'> (got 'a string' that is a <class 'str'>).", ...Code language: Python (python)
Since we tried to create an object C with a string value for x, the instance_of validator throws an error because it requires an int type instead of a string. Note that the type annotation x: int alone doesn’t check anything at runtime. It’s the validator that does the work.
Let’s now define our own validator, this time with the decorator method:
from attrs import define, field
@define
class DividableByTwo:
x: int = field()
@x.validator
def check(self, attribute, value):
if value % 2 != 0:
raise ValueError(f'{value} is not dividable by 2')
print(DividableByTwo(60))
print(DividableByTwo(11))
# Output will be something like:
# DividableByTwo(x=60)
# ...
# ValueError: 11 is not dividable by 2Code language: Python (python)
With @define, validators also run when you assign a new value to an attribute later on, so obj.x = 11 raises the same error.
Python attrs converter example
A converter takes the value that is set and converts it automatically. You can use this for all kinds of purposes. One example is to automatically convert a value to an int. Again, let’s start with using a callable function, in this case, we simply use Python’s int() function:
from attrs import define, field
@define
class C:
x: int = field(converter=int)
c = C("1")
print(c)
# Output:
# C(x=1)Code language: Python (python)
Our input (the string “1”) was converted to an integer automatically. Because converters are run before validators, you can validate the final value after conversion. E.g., you could combine the above two examples to first convert any input to int, and then check if the value is dividable by two.
Using slots with Python attrs
Finally, attrs can create slotted classes for you. Slotted classes have some advantages over regular classes:
- They have a small memory footprint
- They are faster
In short, with a slotted class you explicitly state which instance attributes you expect your object instances to have. This way, Python can leave out some checks and such, resulting in less memory usage and slight speed increases. You can find more details in the attrs documentation.
However, slotted classes come with caveats too, especially when you manually create them. Luckily, with attrs you don’t have to do anything: classes created with @define are slotted by default. A side effect is that you can’t add attributes that you didn’t declare:
from attrs import define
@define
class Point:
x: int
y: int
p = Point(1, 2)
p.z = 3
# AttributeError: 'Point' object has no attribute 'z' and no __dict__ for setting new attributesCode language: Python (python)
That’s usually what you want, since it catches typos in attribute names. If you do need a regular class, turn slots off:
from attrs import define
@define(slots=False)
class YourClassName:
...
Keep learning
- Our article on Python data classes
- Our tutorial on Python classes and objects
- For more examples, check out the attrs examples page