Python dataclass decorator
last modified October 30, 2026 Python 3.15
In this article we show how to use the dataclass decorator in
Python. The decorator removes much of the boilerplate code of classes that
mainly hold data.
The dataclass decorator
The dataclass decorator from the dataclasses module
examines a class to find its fields and generates special methods such as
__init__, __repr__, and __eq__. A
field is a class variable that has a type annotation.
@dataclass
class Test:
...
@dataclass()
class Test:
...
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False,
frozen=False, match_args=True, kw_only=False, slots=False,
weakref_slot=False)
class Test:
...
These three declarations are equivalent. If no parameters are given, the defaults are used.
| Parameter | Effect when True |
|---|---|
init | generates __init__; ignored if the class defines it |
repr | generates __repr__; ignored if the class defines it |
eq | generates __eq__, which compares the fields as tuples |
order | generates __lt__, __le__, __gt__, __ge__; raises TypeError if the class defines any of them |
unsafe_hash | forces generating __hash__; by default it depends on eq and frozen |
frozen | assigning to fields raises FrozenInstanceError |
match_args | creates __match_args__ for positional patterns |
kw_only | all fields become keyword-only in __init__ |
slots | creates a class with __slots__ |
weakref_slot | adds a __weakref__ slot; requires slots=True |
A regular class
In a regular class, we write the constructor and other special methods manually.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f'Person(name={self.name!r}, age={self.age!r})'
p1 = Person('John Doe', 34)
p2 = Person('John Doe', 34)
print(p1)
print(p1 == p2)
The Person class has a constructor and a __repr__
method, which gives a representation of the object. We have not defined
__eq__, so two persons with the same data are not equal; the
default comparison checks object identity.
$ python regular_class.py Person(name='John Doe', age=34) False
A simple dataclass
The following example rewrites the class with the dataclass
decorator.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
p1 = Person('John Doe', 34)
p2 = Person('John Doe', 34)
p3 = Person(name='Roger Roe', age=41)
print(p1)
print(p3)
print(p1 == p2)
print(p1 == p3)
The class has two fields: name and age.
@dataclass
class Person:
name: str
age: int
The decorator generates __init__(self, name: str, age: int),
__repr__, and __eq__. The order of the parameters
follows the order of the fields.
print(p1 == p2)
The generated __eq__ compares the fields, so two objects with
the same data are equal.
$ python simple_dataclass.py Person(name='John Doe', age=34) Person(name='Roger Roe', age=41) True False
Default values
Fields can have default values.
from dataclasses import dataclass
@dataclass
class Person:
name: str = 'unknown'
age: int = 0
p1 = Person('John Doe', 34)
print(p1)
p2 = Person()
print(p2)
p3 = Person(age=21)
print(p3)
With the assignment operator, we give the fields default values. When we do not pass the values to the constructor, the defaults are used.
$ python default_values.py Person(name='John Doe', age=34) Person(name='unknown', age=0) Person(name='unknown', age=21)
Field order
Like function parameters, fields without a default value cannot follow fields with a default value.
from dataclasses import dataclass
try:
@dataclass
class Person:
name: str = 'unknown'
age: int
except TypeError as e:
print(e)
The error is raised when the class is created, because the generated
__init__ would be invalid. Keyword-only fields, described later,
lift this restriction.
$ python field_order.py non-default argument 'age' follows default argument 'name'
Mutable default values
A mutable default value such as a list would be shared by all instances.
Dataclasses reject such defaults; we use field(default_factory=...)
instead.
from dataclasses import dataclass, field
try:
@dataclass
class Post:
title: str
tags: list[str] = []
except ValueError as e:
print(e)
@dataclass
class Post:
title: str
tags: list[str] = field(default_factory=list)
p1 = Post('First post')
p2 = Post('Second post')
p1.tags.append('python')
print(p1)
print(p2)
The first class definition fails with a ValueError. In the
second one, default_factory=list calls list() for
each new instance, so every post gets its own list.
$ python mutable_default.py mutable default <class 'list'> for field tags is not allowed: use default_factory Post(title='First post', tags=['python']) Post(title='Second post', tags=[])
Frozen dataclasses
If the frozen parameter is set to True, the
instances are immutable.
from dataclasses import dataclass, FrozenInstanceError, replace
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(3, 4)
try:
p.x = 10
except FrozenInstanceError as e:
print(e)
p2 = replace(p, x=10)
print(p, p2)
distances = {Point(0, 0): 0, p: 5}
print(distances[Point(3, 4)])
Assigning to a field raises FrozenInstanceError. To get a
modified copy, we use the replace function.
distances = {Point(0, 0): 0, p: 5}
With frozen=True and eq=True, the decorator
generates __hash__, so the points can be used as dictionary keys.
A regular dataclass sets __hash__ to None and is
unhashable.
$ python frozen.py cannot assign to field 'x' Point(x=3, y=4) Point(x=10, y=4) 5
Ordering
With order=True, the instances can be compared with the
<, <=, >, and
>= operators. The fields are compared as tuples, in the order
in which they are defined.
from dataclasses import dataclass
@dataclass(order=True)
class Version:
major: int
minor: int
patch: int
versions = [Version(3, 14, 2), Version(3, 9, 20), Version(3, 15, 0),
Version(3, 14, 0)]
for version in sorted(versions):
print(version)
print(Version(3, 9, 20) < Version(3, 14, 0))
print(max(versions))
Version numbers compare correctly: 3.9.20 is lower than 3.14.0, which would
not be the case if we compared them as strings. Since the instances are
comparable, we can use sorted and max.
$ python ordering.py Version(major=3, minor=9, patch=20) Version(major=3, minor=14, patch=0) Version(major=3, minor=14, patch=2) Version(major=3, minor=15, patch=0) True Version(major=3, minor=15, patch=0)
Excluding fields from comparisons
The compare parameter of the field function
excludes a field from the generated comparison methods.
from dataclasses import dataclass, field
@dataclass(order=True)
class Task:
priority: int
name: str = field(compare=False)
tasks = [Task(3, 'write tests'), Task(1, 'fix bug'), Task(2, 'review'),
Task(1, 'deploy')]
for task in sorted(tasks):
print(task)
print(Task(1, 'a') == Task(1, 'b'))
The tasks are sorted only by their priority. Python's sort is stable, so tasks with the same priority keep their original order. Since the name is not compared, two tasks with the same priority are equal.
$ python sort_index.py Task(priority=1, name='fix bug') Task(priority=1, name='deploy') Task(priority=2, name='review') Task(priority=3, name='write tests') True
The asdict and astuple functions
The asdict function converts a dataclass instance to a
dictionary of its fields; astuple converts it to a tuple.
from dataclasses import dataclass, asdict, astuple
import json
@dataclass
class Address:
city: str
country: str
@dataclass
class Person:
name: str
occupation: str
age: int
address: Address
p = Person('John Doe', 'gardener', 34, Address('Bratislava', 'Slovakia'))
print(p)
print(asdict(p))
print(astuple(p))
print(json.dumps(asdict(p), indent=2))
Both functions work recursively: the nested Address dataclass
becomes a nested dictionary or tuple. A dictionary can be serialized to JSON
directly.
$ python as_dict_fun.py
Person(name='John Doe', occupation='gardener', age=34, address=Address(city='Bratislava', country='Slovakia'))
{'name': 'John Doe', 'occupation': 'gardener', 'age': 34, 'address': {'city': 'Bratislava', 'country': 'Slovakia'}}
('John Doe', 'gardener', 34, ('Bratislava', 'Slovakia'))
{
"name": "John Doe",
"occupation": "gardener",
"age": 34,
"address": {
"city": "Bratislava",
"country": "Slovakia"
}
}
The field function
With the field function, we can provide additional per-field
information.
from dataclasses import dataclass, field
@dataclass
class Person:
name: str
age: int
occupation: str = field(init=False, repr=False)
p = Person('John Doe', 34)
print(p)
p.occupation = 'gardener'
print(f'{p.name} is a {p.occupation}')
In the example, we have an additional occupation field.
occupation: str = field(init=False, repr=False)
The occupation field is not included in the
__init__ and __repr__ methods. We set it after the
object is created.
$ python fields_fun.py Person(name='John Doe', age=34) John Doe is a gardener
The __post_init__ method
The generated __init__ calls the __post_init__
method, if it is defined. It is used to validate the data and to compute
derived fields.
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False)
def __post_init__(self):
if self.width <= 0 or self.height <= 0:
raise ValueError('sides must be positive')
self.area = self.width * self.height
r = Rectangle(3, 4.5)
print(r)
try:
Rectangle(-1, 2)
except ValueError as e:
print(f'error: {e}')
The area field is excluded from __init__ and
computed from the sides. Invalid sides raise a ValueError.
$ python post_init.py Rectangle(width=3, height=4.5, area=13.5) error: sides must be positive
Keyword-only fields
Keyword-only fields must be passed by name. This makes calls with many parameters easier to read.
from dataclasses import dataclass, KW_ONLY
@dataclass(kw_only=True)
class Connection:
host: str
port: int = 5432
timeout: float = 10.0
@dataclass
class Message:
text: str
_: KW_ONLY
urgent: bool = False
retries: int = 3
print(Connection(host='localhost', timeout=2.5))
print(Message('hello', urgent=True))
try:
Connection('localhost', 5432)
except TypeError as e:
print(e)
With kw_only=True, all fields of Connection are
keyword-only. In Message, the fields after the
KW_ONLY pseudo-field are keyword-only; text is
still positional. Passing keyword-only fields positionally raises a
TypeError.
$ python kw_only.py Connection(host='localhost', port=5432, timeout=2.5) Message(text='hello', urgent=True, retries=3) Connection.__init__() takes 1 positional argument but 3 were given
Slots
With slots=True, the decorator creates a class with
__slots__. Instances store their fields in fixed slots instead
of a per-instance dictionary, so they use less memory and their attributes
are accessed faster.
from dataclasses import dataclass
import sys
@dataclass
class Point:
x: int
y: int
@dataclass(slots=True)
class SlotPoint:
x: int
y: int
p = Point(1, 2)
sp = SlotPoint(1, 2)
print(hasattr(p, '__dict__'), hasattr(sp, '__dict__'))
print(SlotPoint.__slots__)
print(sys.getsizeof(p) + sys.getsizeof(p.__dict__), sys.getsizeof(sp))
p.z = 3
try:
sp.z = 3
except AttributeError as e:
print(e)
The regular point has a __dict__; the slotted one does not. We
compare the memory used by the objects and their dictionaries. New attributes
cannot be added to a slotted instance.
$ python slots.py
True False
('x', 'y')
344 48
'SlotPoint' object has no attribute 'z' and no __dict__ for setting new attributes
Inheritance
A dataclass can inherit from another dataclass. The fields of the base class come first.
from dataclasses import dataclass, fields
@dataclass
class Person:
name: str
age: int
@dataclass
class Employee(Person):
salary: int
department: str = 'sales'
e = Employee('John Doe', 34, 3100)
print(e)
print(isinstance(e, Person))
print([f.name for f in fields(e)])
The Employee class adds two fields to the inherited ones. The
fields function returns the fields of a dataclass as
Field objects.
$ python inheritance.py Employee(name='John Doe', age=34, salary=3100, department='sales') True ['name', 'age', 'salary', 'department']
Type annotations are not enforced
The annotations define the fields, but they are not checked at runtime.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
p = Person(34, 'John Doe')
print(p)
print(Person.__annotations__)
We create a person with the arguments swapped, and no error is raised. Types
can be checked statically with a tool such as mypy, or validated in
__post_init__.
$ python no_validation.py
Person(name=34, age='John Doe')
{'name': <class 'str'>, 'age': <class 'int'>}
Dataclass with pattern matching
The next example uses a dataclass with the match statement.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
def check(p):
match p:
case Point(x=0, y=0):
print('Origin')
case Point(x, y) if y == 0:
print(f'{p} on x axis')
case Point(x, y) if x == 0:
print(f'{p} on y axis')
case Point(x, y) if x > 0 and y > 0:
print(f'{p} in Q I')
case Point(x, y) if x < 0 and y > 0:
print(f'{p} in Q II')
case Point(x, y) if x < 0 and y < 0:
print(f'{p} in Q III')
case Point():
print(f'{p} in Q IV')
case _:
print('Not a point')
points = [Point(3, 0), Point(0, 0), Point(-4, -5), Point(-4, 0), Point(0, 5),
Point(4, 8), Point(-5, 3), Point(6, -4), (1, 2)]
for p in points:
check(p)
print(Point.__match_args__)
We have a list of Point objects. With match/case,
we assign each point to the origin, the x or y axis, or one of the four
quadrants.
case Point(x=0, y=0):
print('Origin')
In this case arm, we match a point which has x=0 and y=0 coordinates.
case Point(x, y) if x > 0 and y > 0:
print(f'{p} in Q I')
Positional patterns such as Point(x, y) work because the
decorator creates the __match_args__ attribute. The guard checks
if the point lies in the first quadrant. The tuple at the end of the list is
not a Point, so it falls through to the wildcard case.
$ python points.py
Point(x=3, y=0) on x axis
Origin
Point(x=-4, y=-5) in Q III
Point(x=-4, y=0) on x axis
Point(x=0, y=5) on y axis
Point(x=4, y=8) in Q I
Point(x=-5, y=3) in Q II
Point(x=6, y=-4) in Q IV
Not a point
('x', 'y')
Source
dataclasses - Data Classes - Python documentation
In this article we have worked with Python dataclass decorators.
Author
List all Python tutorials.