Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Python’s dataclasses module turns an annotated class into a concise record-like model. The @dataclass decorator can generate an initializer, representation, equality, and other methods from the fields you declare, while leaving your own methods under the rules for each option. It is part of the standard library (added in Python 3.7), so no third-party package is required.

Dataclasses describe structure; they are not runtime type validators. An annotation such as quantity: int identifies a field for dataclass processing, but the generated code does not generally reject a string assigned to that field. Use a separate validation step when values come from untrusted or loosely typed input.

How do I use dataclasses for structured application data in Python?

Import dataclass, annotate the attributes that make up one record, and apply the decorator:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from dataclasses import dataclass

@dataclass
class InventoryItem:
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

item = InventoryItem("Notebook", 4.50, 12)
print(item)                 # InventoryItem(name='Notebook', unit_price=4.5, quantity_on_hand=12)
print(item.total_cost())    # 54.0

The decorator examines annotated class variables and, unless excluded as special cases, treats them as fields in class-definition order. It can generate __init__, __repr__, and __eq__; ordering methods such as __lt__ are opt-in. A method you write yourself is not blindly overwritten: the result depends on the specific decorator parameter and whether the class already defines that method. The official behavior is documented at Python’s dataclasses documentation.

What the generated initializer means

For the example, the generated signature is effectively InventoryItem(name, unit_price, quantity_on_hand=0). Fields without defaults must come before fields with defaults, including fields inherited from a base dataclass. Violating that rule raises TypeError while the class is created, rather than when an instance is constructed.

Annotations identify fields, but do not validate values

This succeeds unless your own code rejects it:

bad = InventoryItem("Notebook", "free", 12)  # no automatic int/float validation

Static type checkers can flag the mismatch, and a validation layer can enforce constraints at an input boundary. Keep those responsibilities separate from dataclass generation; the decorator itself generally does not inspect the annotated type. ClassVar annotations and InitVar parameters are notable exceptions to ordinary stored fields.

Field defaults: constants versus per-instance values

Use a normal default for an immutable-like constant

A scalar default is appropriate when every instance can start with the same value:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from dataclasses import dataclass

@dataclass
class RetryPolicy:
    attempts: int = 3
    backoff_seconds: float = 1.0

Give mutable fields a fresh value with default_factory

Never place one shared list, dictionary, or set directly in a field default. Use a zero-argument factory so each instance receives a new object:

from dataclasses import dataclass, field

@dataclass
class UserProfile:
    username: str
    roles: list[str] = field(default_factory=list)
    preferences: dict[str, str] = field(default_factory=dict)

first = UserProfile("ada")
second = UserProfile("grace")
first.roles.append("admin")
assert second.roles == []

The factory is called only when a default is needed and must accept no arguments. In Python 3.11, dataclasses reject unhashable defaults as a practical approximation for detecting mutable values; earlier documentation described checks specifically for list, dict, and set. This safeguard is not a substitute for thinking about ownership of nested objects.

Customize a field’s participation

field() can independently control whether a value appears in the generated initializer (init), representation (repr), equality and ordering (compare), hashing (hash), or keyword-only parameters (kw_only). Its metadata mapping is reserved for third-party extensions; dataclasses itself does not consume it.

from dataclasses import dataclass, field

@dataclass
class ApiToken:
    token: str = field(repr=False)
    created_by: str = field(compare=False)
    scopes: tuple[str, ...] = ()

Here the token is omitted from the representation and created_by does not affect generated comparisons. Make these exclusions only when they express a real security, API, or comparison requirement.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Equality, ordering, hashing, and frozen instances

Equality is type-sensitive and field-based

With defaults, two dataclass instances compare as tuples of their fields, in declaration order, and they must have the identical class type:

@dataclass
class Point:
    x: int
    y: int

Point(1, 2) == Point(1, 2)  # True

A different class containing the same values is not considered equal. Fields marked compare=False are left out of generated equality (and ordering) comparisons.

Ordering must be requested deliberately

Set order=True to generate __lt__, __le__, __gt__, and __ge__ using field order. Ordering is disabled by default. Requesting ordering while disabling equality (eq=False) raises ValueError, because the generated ordering relies on equality semantics.

@dataclass(order=True)
class Priority:
    level: int
    label: str

Use ordering only when lexicographic field order represents a meaningful business order; otherwise write an explicit key or comparison method.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frozen means assignment-style read-only behavior

frozen=True adds generated attribute methods that raise FrozenInstanceError when code assigns to or deletes a field:

from dataclasses import dataclass

@dataclass(frozen=True)
class Coordinate:
    latitude: float
    longitude: float

point = Coordinate(51.5, -0.1)
# point.latitude = 52.0  # raises FrozenInstanceError

Python’s documentation states: “It is not possible to create truly immutable Python objects.” Frozen dataclasses emulate immutability for normal attribute assignment; a nested list or dictionary can still be mutable, and other escape hatches in Python remain possible.

Hashing follows eq and frozen

When both equality and freezing are enabled, a hash is generated by default. A class with equality enabled but not frozen is normally unhashable, because mutation could invalidate its hash. unsafe_hash=True forces a hash in specialized cases and should be used cautiously: hashability does not make mutable state safe to change while an object is in a set or dictionary key.

Constructor and memory-layout options

The decorator’s options solve different problems. Their availability also depends on the Python version:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Option Added Effect Important constraint
kw_only=True Python 3.10 All generated initializer parameters are keyword-only. Callers must use names such as Item(name="x").
slots=True Python 3.10 Generates __slots__ and returns a new class. Changes instance layout and can affect inheritance and code expecting __dict__.
weakref_slot=True Python 3.11 Adds a __weakref__ slot. Requires slots=True.
frozen=True Python 3.7 Blocks ordinary assignment and deletion. It is not deep immutability.
default_factory=... Python 3.7 Creates a default value per instance. The factory must take no arguments.

Keyword-only APIs

Keyword-only fields make call sites self-documenting and let you add optional parameters without relying on positional order:

from dataclasses import dataclass

@dataclass(kw_only=True)
class Connection:
    host: str
    port: int = 443
    timeout: float = 5.0

connection = Connection(host="api.example", timeout=10.0)

The class-level option was added in Python 3.10. Individual fields can also use field(kw_only=True) when only selected parameters should require names.

Slots and weak references

slots=True creates a slotted dataclass and returns a new class rather than simply mutating the original class object. This can impose useful layout constraints, but do not promise a universal speed or memory gain: results depend on Python version, inheritance, and workload, so benchmark your application if that trade-off matters. In Python 3.11, inherited slot names are not duplicated. Because a slot name is not a reliable field inventory, use fields() instead of inspecting __slots__. Add weakref_slot=True only when weak references are required; it is available from Python 3.11 and requires slots.

Lifecycle hooks and inheritance

Validate or derive values in __post_init__

The generated initializer calls __post_init__() if the class defines it. This is a convenient place to derive attributes or perform explicit checks:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
from dataclasses import dataclass, field

@dataclass
class OrderLine:
    unit_price: float
    quantity: int
    total: float = field(init=False)

    def __post_init__(self) -> None:
        if self.unit_price < 0 or self.quantity < 0:
            raise ValueError("price and quantity must be non-negative")
        self.total = self.unit_price * self.quantity

A generated initializer does not automatically call a non-dataclass base class’s __init__. If that base initialization is needed, call it from __post_init__ (or provide another explicit construction path).

Use InitVar for construction-only input

An InitVar appears as an initializer parameter and is passed to __post_init__, but it is not stored as a normal field. This is useful for a password, configuration object, or other temporary construction input. It is excluded from fields() and ordinary serialization.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Inspecting fields and converting dataclasses

Enumerate real fields with fields()

fields() accepts a dataclass class or instance and returns Field objects. It excludes pseudo-fields such as ClassVar and InitVar:

from dataclasses import fields

for definition in fields(OrderLine):
    print(definition.name, definition.type, definition.init)

This is the supported way to discover dataclass fields for generic UI, logging, or mapping code. It is more robust than reading __annotations__ or slots because it reflects dataclass-specific exclusions and options.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How do I convert a dataclass to a dict?

Call asdict() when you want a recursively converted data structure:

from dataclasses import asdict

payload = asdict(order_line)

asdict() recursively converts nested dataclass instances, dictionaries, lists, and tuples. For other objects it applies copy.deepcopy(). Consequently, it is not a shallow view and can copy large or identity-sensitive values.

For a shallow mapping that reads the current field values without recursive conversion, iterate over fields() yourself:

from dataclasses import fields

def shallow_dict(instance):
    return {definition.name: getattr(instance, definition.name)
            for definition in fields(instance)}

Choose the shallow form when preserving object identity matters or when deep copying would be unnecessarily expensive. Neither form automatically turns arbitrary values into JSON; a JSON encoder still needs values it understands.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Copy with changes using replace()

replace(instance, **changes) constructs a new instance through the class initializer, so __post_init__ runs again:

from dataclasses import replace

updated = replace(order_line, quantity=3)

Fields declared with init=False are not copied by replace(). They must be recomputed by __post_init__ or handled through a deliberate custom method. Be especially careful when such fields hold caches, handles, or other state that cannot be reconstructed from initializer arguments.

A decision checklist for production models

  • Need generated construction and representation? Start with plain @dataclass and annotations.
  • Need a mutable collection per object? Use field(default_factory=...), never a shared mutable literal.
  • Need input checks? Implement explicit validation or use a dedicated validation layer; annotations alone do not enforce types.
  • Should callers name every argument? Use kw_only=True (Python 3.10 or newer).
  • Should instances reject normal reassignment? Consider frozen=True, while documenting that nested values are not deeply immutable.
  • Does field order define a legitimate sort order? Opt into order=True; otherwise keep ordering off.
  • Will objects be dictionary keys? Review eq, frozen, and mutation together before relying on generated hashing.
  • Do you need slots or weak references? Confirm Python 3.10/3.11 availability and test inheritance, introspection, and framework compatibility.
  • Need serialization? Decide between recursive asdict(), a shallow fields() mapping, and an explicit format for non-JSON values.

For the complete parameter rules, generated-method behavior, and version notes, consult the Python 3.11 dataclasses reference and the design rationale in PEP 557.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.