The dataclasses module in the Python standard library (added in Python 3.7) eliminates the boilerplate of writing init, repr, and eq for classes that primarily store data. You define fields with type annotations, add the @dataclass decorator, and the methods are generated automatically.
Without dataclasses, a simple data class needs a lot of repetitive code just to get equality checks and a readable string representation working correctly:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.yWith dataclasses, the same thing is three lines, and the decorator writes the init, repr, and eq methods shown above for you automatically:
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p1 = Point(1.5, 2.5)
p2 = Point(1.5, 2.5)
print(p1) # Point(x=1.5, y=2.5)
print(p1 == p2) # True@dataclass generates init, repr, and eq from the annotated fields. The class behaves exactly like the hand-written version.
Default values
Not every field needs a value at construction time. Provide defaults by assigning values directly to fields in the class body, the same way you would set a class attribute. Fields with defaults must come after fields without defaults, or Python raises a TypeError when the class is defined.
from dataclasses import dataclass
@dataclass
class User:
name: str
active: bool = True
score: int = 0
u = User("Maya")
print(u) # User(name='Maya', active=True, score=0)Simple defaults like True and 0 work fine directly on the field, but mutable defaults need a different approach. For mutable defaults like lists or dictionaries, use field(default_factory=...) instead of a direct default value. Direct mutable defaults are shared across all instances, which is almost never what you want.
from dataclasses import dataclass, field
@dataclass
class ShoppingCart:
items: list = field(default_factory=list)
owner: str = "guest"
cart1 = ShoppingCart()
cart2 = ShoppingCart()
cart1.items.append("apple")
print(cart1.items) # ['apple']
print(cart2.items) # []default_factory=list creates a new empty list for each instance. Each cart's items list is independent, so appending to one never affects the other.
Immutable dataclasses
Pass frozen=True to make all fields read-only after initialization, which is useful for configuration objects and values you never want mutated once created.
from dataclasses import dataclass
@dataclass(frozen=True)
class Config:
host: str
port: int = 8080
debug: bool = False
config = Config("localhost")
print(config.host)
config.port = 9000Reading host works normally, but the assignment to port on the last line raises an exception instead of silently overwriting the field:
localhost
dataclasses.FrozenInstanceError: cannot assign to field 'port'Frozen dataclasses are hashable (if all fields are hashable), so they can be used as dictionary keys or set members. They behave like immutable records.
Post-initialization with post_init
Use post_init to run validation or compute derived fields after the generated init runs. It runs automatically right after the fields are assigned, with no extra call needed.
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("Dimensions must be positive")
self.area = self.width * self.heightWith the class defined, creating a rectangle computes and validates the area automatically, without the caller having to pass it in or call a separate method:
r = Rectangle(10, 5)
print(r.area) # 50field(init=False) excludes area from the generated init. post_init calculates it from the other fields and validates the input.
Converting to and from dictionaries
Use dataclasses.asdict() and dataclasses.astuple() to convert instances to dictionaries and tuples.
from dataclasses import dataclass, asdict, astuple
@dataclass
class Book:
title: str
author: str
year: int
b = Book("Dune", "Frank Herbert", 1965)
print(asdict(b)) # {'title': 'Dune', 'author': 'Frank Herbert', 'year': 1965}
print(astuple(b)) # ('Dune', 'Frank Herbert', 1965)asdict() is useful for serializing to JSON. For nested dataclasses, use asdict(b) and then pass to json.dumps.
Ordering and comparison
By default, dataclasses support == and != by comparing all fields for equality. To add ordering (<, >, <=, >=), pass order=True so instances can be sorted directly.
| Decorator option | Supports | Comparison basis |
|---|---|---|
| @dataclass (default) | ==, != | All fields, in definition order |
| @dataclass(order=True) | ==, !=, <, >, <=, >= | All fields, in definition order |
from dataclasses import dataclass
@dataclass(order=True)
class Player:
score: int
name: str
players = [Player(85, "Devin"), Player(92, "Maya"), Player(78, "Priya")]
for p in sorted(players):
print(p)sorted() uses the generated comparison methods to order the players from lowest score to highest, printing one Player per line:
Player(score=78, name='Priya')
Player(score=85, name='Devin')
Player(score=92, name='Maya')With order=True, instances are compared field by field in definition order, so score is listed first here specifically to sort players by score. Reversing the field order would sort by name instead.
Inheritance
Dataclasses support inheritance the same way regular classes do. The child class inherits fields from the parent and can add its own on top.
from dataclasses import dataclass
@dataclass
class Vehicle:
make: str
model: str
@dataclass
class Car(Vehicle):
doors: int = 4
c = Car("Tesla", "Model 3")
print(c)The generated init accepts the parent's fields first, followed by the child's own fields, in the order both classes declare them:
Car(make='Tesla', model='Model 3', doors=4)Fields from the parent come first in init. A child can override a parent's default value by re-declaring the field.
Practical example: API response model
Use dataclasses to model API responses with type safety and automatic serialization. Nested dataclasses, like an Address inside a User, work the same as any other field type.
from dataclasses import dataclass, field, asdict
from typing import List
import json
@dataclass
class Address:
street: str
city: str
zipcode: strAddress is a plain nested dataclass. User references it as a field type, with a default_factory so each user starts with its own empty address list:
@dataclass
class User:
id: int
name: str
email: str
addresses: List[Address] = field(default_factory=list)
active: bool = TrueWith both classes defined, build a user with one nested address and serialize the whole structure to JSON in a single call:
address = Address("123 Main St", "Springfield", "62701")
user = User(1, "Maya", "maya@example.com", [address])
print(json.dumps(asdict(user), indent=2))asdict() recursively converts nested dataclasses, so the Address instance turns into a plain dictionary inside the output automatically, with indent=2 producing readable, pretty-printed JSON:
{
"id": 1,
"name": "Maya",
"email": "maya@example.com",
"addresses": [
{
"street": "123 Main St",
"city": "Springfield",
"zipcode": "62701"
}
],
"active": true
}The result is a plain dictionary tree with no dataclass instances left in it, ready to pass to json.dumps or any other serializer.
Common mistakes
Using a mutable default directly. Writing items: list = [] shares the same list across all instances. Use field(default_factory=list) instead.
Forgetting that field order matters. In init, fields appear in definition order. Fields with defaults must come after fields without defaults, or Python raises TypeError.
Using dataclass when a namedtuple would be simpler. If you need immutability, tuple unpacking, and indexing, namedtuple is lighter. See collections.namedtuple for comparison.
Overriding init without calling post_init. If you write your own init, the generated post_init is not called automatically. Either avoid custom init or call self.post_init() at the end of your implementation.
Rune AI
Key Insights
@dataclassgenerates__init__,__repr__, and__eq__automatically from type-annotated fields.- Use
field(default=...)for mutable defaults andfield(default_factory=...)for lists, dicts, and sets. - Use
frozen=Truefor immutable, hashable dataclasses. - Use
__post_init__for validation or derived fields after the generated__init__runs. - Dataclasses support inheritance and can be compared, sorted, and used with type checkers.
- Use
dataclasses.asdict()to convert a dataclass instance to a dictionary.
Frequently Asked Questions
When should I use a dataclass instead of a regular class?
How do I make a dataclass immutable?
What is the difference between dataclass and namedtuple?
Conclusion
dataclasses eliminate the repetitive boilerplate of writing init, repr, and eq for data-focused classes. Use @dataclass for models, configuration objects, API responses, and any class where the primary purpose is storing named values. Add frozen=True when you need immutability and hashability.
More in this topic
Format Dates and Times in Python
Learn how to use strftime and strptime to format dates as strings and parse date strings into Python datetime objects.
What Is the Python Standard Library?
Learn what the Python standard library is, why it matters, and how to use its built-in modules without installing anything extra.
Encode and Decode Base64 Data in Python
Learn how to use Python's base64 module to encode binary data as text and decode Base64 strings back to bytes.