from dataclasses import dataclass
@dataclass(frozen=True)
class FrozenDataClass:
int
a: int
b:
= FrozenDataClass(1,2)
frozen = 3 frozen.c
FrozenInstanceError: cannot assign to field 'c'
dataclasses
or attrs
?dataclasses
or attrs
? This article will give you an overview with examples.
noklam
April 22, 2022
This blog goes into detail with examples of using dataclasses
and attrs
, why and when you should consider to use it. This assume you already understand why dataclass and its variants are useful, so I am not trying to convince you that you should use dataclass, but WHICH libraries you may want to choose.
If you are looking for a quick summary:
Item | dataclasses | attrs |
---|---|---|
Immutable Instance | ✅ @dataclass(frozen=True) | @define(frozen=True) |
Immutable Field | ❌ | ✅ |
Derived Attributes | ✅ | ✅ |
Derived Attributes + Immutability | ❌ | ✅ |
Dependencies | ✅ standard library | ✅ almost zero dependency |
With dataclasses
, you can set frozen=True
to ensure immutablilty. It throws an FrozenInstanceError
when someone is trying to update an immutable object.
from dataclasses import dataclass
@dataclass(frozen=True)
class FrozenDataClass:
a: int
b: int
frozen = FrozenDataClass(1,2)
frozen.c = 3
FrozenInstanceError: cannot assign to field 'c'
With attrs
, it’s mostly identical except that you use @define(frozen=True)
.
Sometimes attribute are not defined during initialisation, but derived from other attribtues.
@dataclass
class DataClass:
a: int
b: int
def __post_init__(self):
self.c = self.a + self.b
frozen = DataClass(1,2)
print(frozen.c)
3
Similarly, with attrs
:
dataclasses
does not have this flexibility. Here is an example with attrs
:
Now you get a new FrozenAttributeError
error. What if you want to set attributes on a frozen class?
post_init
assignment in a frozen dataclass ✾For those of you thinking about using derived attribute with dataclass
, it doesn’t work.
@dataclass(frozen=True)
class FrozenDataClass:
a: int
b: int
def __post_init__(self):
self.c = self.a + self.b
frozen = FrozenDataClass(1,2)
FrozenInstanceError: cannot assign to field 'c'
It doesn’t work! Because the frozen flag will block any assignment even in the __post_init__
method assignment too.
object.__setattr__
trickAll Python objects are just regular objects, thus they aren’t truely “immutable”. Most of the time, the libraries achieve the immutability via implementing the __setattr__
method.
FrozenInstanceError:
It may seems like it is indeed immutable, but if you try hard enough you can always crack it.
The object
class is almost like the parent of all class. So that even though frozen_class.__setattr__
works fine, you can still by pass this via this trick. In theory, you could also use this trick to achieve partial immutability with dataclasses
.
We learnt that the frozen dataclass doesn’t work well with derived attributes with dataclasses
. This is so common and probably easier to achieve via the good old @property
. Does that mean dataclass are not useful? This is something that I found unclear when reading through the docs. Luckily attrs
has a solution to this too:
import attrs
from attrs import define
@define(frozen=True)
class FrozenDerivedAttrs:
a: int
b: int
c: int = field(init=False)
@c.default
def _default_value(self):
return self.a + self.b
obj = FrozenDerivedAttrs(1,2)
obj.c
3
The above method is more natural way of writing Python class, but there is another approach that are usually easier to test. Essentially, you use factory method to produce an immutable class.
attrs
offers a lot more flexibility compare to dataclasses
, from frozen class, frozen field, derived attributes and a combination of them (there are a lot more, you should check out attrs by Example). You may be able to achieve similar thing by using the obejct.__setattr__
trick, but I’d also argue if you are trying so hard to fight with the library, you probably shouldn’t use it. I do feel that when I am writing class with attrs
it feels slightly different in the beginning, but they also teach you how you should write your data class in the long run.