Python dataclasses help you build classes whose main job is to store related values. Add the @dataclass decorator, declare fields with type annotations, and Python can create common methods such as __init__(), __repr__(), and __eq__() for you.
You can still add methods, validation, inheritance, and other normal class features. Dataclasses reduce repeated setup code without turning the class into a different type of object.
Why Use Python Dataclasses?
A regular data-focused class often repeats field assignments, display logic, and comparisons. A dataclass generates those parts from one clear field definition. This makes small models, configuration objects, parsed records, and function results easier to read and maintain.
- Write less repeated class code.
- See required fields and defaults in one place.
- Compare instances by their field values.
- Keep normal Python methods and inheritance.
Note: Type annotations describe the fields, but dataclasses do not enforce those types at runtime. Add validation when your program must reject invalid values.
Create Your First Dataclass
Import dataclass from the standard dataclasses module and place the decorator above the class. Fields without defaults must come before fields with defaults.
Example:
from dataclasses import dataclass
@dataclass
class Course:
# Required fields appear first
title: str
instructor: str
lessons: int = 0
python_course = Course("Python Basics", "Asha", 18)
# Generated __repr__() shows the field values
print(python_course)
# Generated __eq__() compares matching fields
print(python_course == Course("Python Basics", "Asha", 18))
The decorator uses the fields in declaration order. By default, it generates an initializer, a readable representation, and value-based equality. It does not generate ordering methods unless you request them.
Control Generated Methods
You can pass options to @dataclass when the defaults do not fit your class.
| Option | Purpose | Default |
|---|---|---|
| init | Generates the initializer | True |
| repr | Generates a readable object representation | True |
| eq | Compares instances by field values | True |
| order | Generates less-than and other ordering methods | False |
| frozen | Blocks normal field assignment after creation | False |
| slots | Generates slots for the declared fields | False |
| kw_only | Makes generated initializer fields keyword-only | False |
For example, order=True compares instances as ordered tuples of their comparable fields. Use it only when the field order represents a meaningful sort order.
Example:
from dataclasses import dataclass
@dataclass(order=True, slots=True)
class Score:
# Points is checked before player during ordering
points: int
player: str
scores = [Score(92, "Oliver"), Score(88, "Riya")]
# Generated ordering methods let sorted() compare objects
print(sorted(scores))
Use Defaults Safely
Simple immutable values can use normal defaults. Lists, dictionaries, sets, and other mutable values need field(default_factory=...). The factory runs for every new object, so instances do not share one mutable value.
Example:
from dataclasses import dataclass, field
@dataclass
class ReadingList:
owner: str
# Each object receives a separate empty list
books: list[str] = field(default_factory=list)
maya = ReadingList("Maya")
ethan = ReadingList("Ethan")
maya.books.append("Clean Code")
# Ethan's list remains independent
print(maya.books)
print(ethan.books)
Tip: Pass the callable itself to default_factory, such as list or dict. Do not call it with parentheses.
Customize Individual Fields
The field() function controls how one field participates in generated methods. Common options include repr=False, compare=False, init=False, and kw_only=True.
Example:
from dataclasses import dataclass, field
@dataclass
class Account:
username: str
# Keep the token out of the generated representation
access_token: str = field(repr=False, compare=False)
active: bool = True
account = Account("sam", "secret-token")
# The sensitive token is not printed
print(account)
Hiding a field from repr only changes the generated display text. It does not encrypt the value or stop code from reading the attribute.
Validate with __post_init__()
The generated initializer calls __post_init__() after assigning the fields. Use this hook to validate values or calculate a field that depends on other fields.
Example:
from dataclasses import dataclass, field
@dataclass
class InvoiceItem:
unit_price: float
quantity: int
# Users do not pass total to the initializer
total: float = field(init=False)
def __post_init__(self):
# Reject values that would create an invalid invoice
if self.unit_price < 0 or self.quantity < 1:
raise ValueError("Price and quantity must be positive")
# Calculate the derived field after initialization
self.total = self.unit_price * self.quantity
item = InvoiceItem(249.50, 2)
print(item.total)
You can also declare an InitVar when initialization needs a temporary argument that should not become a stored field. The generated initializer passes it to __post_init__().
Create Read-Only-Style Instances
Set frozen=True when normal code should not reassign or delete fields after construction. Attempts to do so raise FrozenInstanceError. Frozen instances work well for stable value objects and dictionary keys when their fields are also hashable.
Example:
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
latitude: float
longitude: float
office = Coordinate(28.6139, 77.2090)
# Frozen and equality-enabled instances can be set members
locations = {office}
print(office in locations)
Note: frozen=True prevents normal attribute assignment, but it does not make mutable values inside the object immutable. Prefer immutable field values when the whole object should remain stable.
Convert and Copy Dataclass Instances
The module includes helpers for common data operations. asdict() recursively converts a dataclass to a dictionary, while astuple() converts it to a tuple. replace() creates another instance with selected values changed.
Example:
from dataclasses import asdict, dataclass, replace
@dataclass(frozen=True)
class Product:
name: str
price: float
in_stock: bool = True
original = Product("Desk Lamp", 1499.00)
# Create a new object instead of changing the frozen one
sale_item = replace(original, price=1199.00)
# Convert nested-friendly data for serialization work
print(asdict(sale_item))
print(original.price)
asdict() performs a recursive conversion and deep-copies other values. For a shallow mapping, build a dictionary from fields() and getattr() instead.
Inherit from a Dataclass
A dataclass can extend another dataclass. Python collects base fields first, then adds or overrides fields from the child class. The combined order also controls the generated initializer.
Example:
from dataclasses import dataclass
@dataclass
class Person:
name: str
email: str
@dataclass
class Student(Person):
# Child fields follow the inherited fields
course: str = "Python"
student = Student("Noah", "[email protected]", "Data Science")
print(student)
Default-order rules apply across inheritance. If a base class has a default field, a child cannot add a required positional field after it. A keyword-only field or a suitable default can resolve that design issue.
Dataclasses and Regular Classes
Use a dataclass when field-based data is central to the object. Choose a regular class when custom construction, hidden state, or behavior matters more than its declared fields.
- Use a dataclass for configuration values, coordinates, results, and small domain records.
- Add
__post_init__()for focused validation or derived values. - Use a regular class or dedicated validation library for complex conversion and validation rules.
- Avoid treating type annotations as runtime validation.
Common Python Dataclass Mistakes
- Placing a required field after a field with a default.
- Using a mutable object as a direct default instead of
default_factory. - Expecting type annotations to reject values automatically.
- Assuming
frozen=Truefreezes lists or dictionaries stored inside the instance. - Including secrets in the generated representation.
- Using
unsafe_hash=Truewithout understanding how mutation affects hashed collections.
Conclusion
Python dataclasses give you concise, readable classes for structured data while preserving normal class behavior. Start with @dataclass, declare required fields before defaults, use default_factory for mutable values, and add __post_init__() when validation is necessary. Options such as frozen, slots, and kw_only let you refine the model without writing repeated boilerplate.