A dataclass writes the boring methods for you. You declare the fields with type annotations and the decorator generates __init__, __repr__ and __eq__.
It has been in the standard library since Python 3.7, so there is nothing to install.
The boilerplate it removes
The old class and the dataclass below it do the same job.
# the boilerplate a dataclass removes
class PointOld:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'PointOld(x={self.x}, y={self.y})'
def __eq__(self, other):
return isinstance(other, PointOld) and (self.x, self.y) == (other.x, other.y)
from dataclasses import dataclass, field, asdict, replace
@dataclass
class Point:
x: int
y: int
p = Point(1, 2)
print(p)
print(p == Point(1, 2))
It prints:
Point(x=1, y=2)
True
Equality compares the fields, not the identity, which is almost always what you want for a value object.
Defaults and the list rule
Simple defaults are written normally. A mutable default needs default_factory, which is called once per instance.
# defaults, and why lists need default_factory
@dataclass
class Basket:
owner: str
items: list = field(default_factory=list)
currency: str = 'INR'
b1, b2 = Basket('ada'), Basket('grace')
b1.items.append('apple')
print(b1)
print(b2)
try:
@dataclass
class Broken:
items: list = []
except ValueError as err:
print(type(err).__name__ + ':', err)
It prints:
Basket(owner='ada', items=['apple'], currency='INR')
Basket(owner='grace', items=[], currency='INR')
ValueError: mutable default <class 'list'> for field items is not allowed: use default_factory
The last lines are the useful part. Python refuses the mutable default rather than letting every instance share one list, which is what happens with a plain function argument.
Frozen instances
frozen=True makes the fields read only and the instance hashable, so it can be a dict key or go in a set.
# frozen instances are hashable and read only
@dataclass(frozen=True)
class Config:
host: str
port: int = 8080
c = Config('localhost')
print(c, hash(c) == hash(Config('localhost')))
try:
c.port = 9090
except Exception as err:
print(type(err).__name__ + ':', err)
It prints:
Config(host='localhost', port=8080) True
FrozenInstanceError: cannot assign to field 'port'
Ordering
order=True generates the comparison methods, which compare the fields in the order they are declared, as a tuple would.
# order gives you comparisons and sorting
@dataclass(order=True)
class Version:
major: int
minor: int
versions = [Version(1, 4), Version(1, 2), Version(0, 9)]
print(sorted(versions))
print(Version(1, 4) > Version(1, 2))
It prints:
[Version(major=0, minor=9), Version(major=1, minor=2), Version(major=1, minor=4)]
True
Sorting works with no key function.
Derived values with __post_init__
field(init=False) keeps a field out of the constructor, and __post_init__ runs straight after it.
# __post_init__ for derived values
@dataclass
class Rect:
width: float
height: float
area: float = field(init=False)
def __post_init__(self):
self.area = self.width * self.height
print(Rect(3, 4))
It prints:
Rect(width=3, height=4, area=12)
asdict and replace
asdict converts to plain dicts, recursively. replace builds a new instance with some fields changed, which is how you update a frozen one.
# asdict and replace
print(asdict(Basket('ada', ['apple'])))
print(replace(Config('localhost'), port=9090))
It prints:
{'owner': 'ada', 'items': ['apple'], 'currency': 'INR'}
Config(host='localhost', port=9090)
Keeping a field out of repr or equality
Useful for secrets, caches and anything noisy.
# fields you do not want in repr or comparison
@dataclass
class User:
name: str
token: str = field(repr=False, compare=False)
print(User('ada', 'secret-token'))
print(User('ada', 'secret-token') == User('ada', 'different-token'))
It prints:
User(name='ada')
True
The token is not printed and does not affect equality.
What to remember
- The decorator generates
__init__,__repr__and__eq__from annotated fields. - Mutable defaults need
field(default_factory=list). frozen=Truegives read only, hashable instances.asdictandreplacecover serialising and updating.
If you also want validation and parsing at the edges of your program, that is where Pydantic earns its place. For plain internal data holders, a dataclass is enough and costs nothing.