Python Dataclasses

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

  1. Placing a required field after a field with a default.
  2. Using a mutable object as a direct default instead of default_factory.
  3. Expecting type annotations to reject values automatically.
  4. Assuming frozen=True freezes lists or dictionaries stored inside the instance.
  5. Including secrets in the generated representation.
  6. Using unsafe_hash=True without 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.



Found This Page Useful? Share It!
Get the Latest Tutorials and Updates
Join us on Telegram