ZetCode

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.

ParameterEffect when True
initgenerates __init__; ignored if the class defines it
reprgenerates __repr__; ignored if the class defines it
eqgenerates __eq__, which compares the fields as tuples
ordergenerates __lt__, __le__, __gt__, __ge__; raises TypeError if the class defines any of them
unsafe_hashforces generating __hash__; by default it depends on eq and frozen
frozenassigning to fields raises FrozenInstanceError
match_argscreates __match_args__ for positional patterns
kw_onlyall fields become keyword-only in __init__
slotscreates a class with __slots__
weakref_slotadds a __weakref__ slot; requires slots=True

A regular class

In a regular class, we write the constructor and other special methods manually.

regular_class.py
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.

simple_dataclass.py
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.

default_values.py
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.

field_order.py
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.

mutable_default.py
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.

frozen.py
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.

ordering.py
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.

sort_index.py
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.

as_dict_fun.py
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.

fields_fun.py
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.

post_init.py
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.

kw_only.py
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.

slots.py
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.

inheritance.py
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.

no_validation.py
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.

points.py
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.