Quick Answer

Decorate a class with @dataclass and declare fields with type annotations. You get __init__, __repr__ and __eq__ generated automatically. Mutable defaults must use field(default_factory=list) or every instance shares one object.

The boilerplate it removes

A plain class holding three fields needs the names written repeatedly:

class Student:
    def __init__(self, name, marks=0, subjects=None):
        self.name = name
        self.marks = marks
        self.subjects = subjects if subjects is not None else []

    def __repr__(self):
        return f"Student({self.name!r}, {self.marks!r}, {self.subjects!r})"

    def __eq__(self, other):
        return (self.name, self.marks, self.subjects) == \
               (other.name, other.marks, other.subjects)

Every field appears four times. Adding one means editing three methods, and forgetting __eq__ is common — which is why comparing two objects that hold identical data often surprisingly returns False.

The same thing as a dataclass

from dataclasses import dataclass, field

@dataclass
class Student:
    name: str
    marks: int = 0
    subjects: list = field(default_factory=list)

That is the whole class, and it generates all three methods.

a = Student("Asha", 91)
print(a)
# Student(name='Asha', marks=91, subjects=[])

b = Student("Asha", 91)
print(a == b)   # True

The __repr__ is genuinely useful — printing a plain object gives <__main__.Student object at 0x...>, which tells you nothing while debugging.

a == b being True is the generated __eq__ comparing field values. Without a dataclass that comparison is identity-based and returns False, which catches people out constantly in tests.

Note the annotations are required. A field without one is not picked up, which is a quiet way to lose a field from the generated methods.

The mutable default trap

This is the one rule you must know, and dataclasses will actually stop you breaking it.

@dataclass
class Bad:
    subjects: list = []      # ValueError at class definition time

Python raises immediately: mutable default values are not allowed. That is a deliberate kindness — the equivalent mistake in a normal function signature fails silently and shares one list across every call.

@dataclass
class Good:
    subjects: list = field(default_factory=list)

p, q = Good(), Good()
p.subjects.append("Physics")
print(q.subjects)   # []  -- separate lists

default_factory takes a callable that is invoked per instance, so each object gets a fresh list. Use it for lists, dicts, sets and any object that could be mutated. It is the same underlying issue described in OOP in Python.

The options worth knowing

@dataclass(frozen=True)
class Point:
    x: int
    y: int

frozen=True makes instances immutable — assigning to a field raises. It also makes them hashable, so they can be dictionary keys or set members. For value objects such as coordinates or currency amounts this is usually what you want, and immutability removes a whole class of accidental-mutation bugs.

order=True generates comparison methods, so instances sort by their fields in declaration order.

field(repr=False) keeps a field out of the printed representation, which matters for anything sensitive — a password hash or token should not appear in logs because someone printed the object.

You can still add ordinary methods. A dataclass is a normal class with generated methods, not a restricted type.

When to use what

Use a dataclass for objects that primarily hold data with a few behaviours — configuration, records, DTOs, coordinates.

Use a NamedTuple when you want an immutable record that also behaves like a tuple, including unpacking. Lighter, but less flexible.

Use a plain dict when the shape is genuinely dynamic or comes straight from JSON and is passed through. A dataclass adds structure precisely when you want the field names fixed and checked.

Use pydantic when the data crosses a boundary and must be validated at runtime — API request bodies especially. Dataclasses do not validate; declaring marks: int does not stop a string being assigned, exactly as covered in type hints.

Dataclasses have been in the standard library since Python 3.7, so there is no dependency cost to using them.

Frequently Asked Questions

Why can I not use a list as a default in a dataclass? Because a single default object would be shared by every instance. Python raises a ValueError to prevent it. Use field(default_factory=list) so each instance gets its own.
Do dataclasses validate types? No. The annotations are hints only, so assigning a string to a field declared int works fine. Use pydantic if you need runtime validation.
What does frozen=True do? It makes instances immutable and hashable, so fields cannot be reassigned and objects can be used as dictionary keys or set members.
Can a dataclass have methods? Yes. It is an ordinary class with generated __init__, __repr__ and __eq__. You can add any methods, properties or class methods you like.
Dataclass or NamedTuple? NamedTuple is immutable, lighter and unpacks like a tuple. A dataclass is more flexible, supports mutability, defaults and inheritance more naturally, and is usually the better default.