Dataclass field policy decides much more than constructor spelling. Defaults, factories, comparison, repr, slots, keyword-only layout, and descriptor behavior all flow through it.
Core answer
Use field options to make generated behavior explicit. Prefer default_factory for mutable values, slots=True when open attribute dictionaries are not part of the contract, and frozen value objects when hashability follows immutability.
# [CURRENT - 3.10-3.14] Works on Python 3.10+ [PEP 557]from dataclasses import dataclass, field@dataclass(slots=True, kw_only=True)class RetryPlan: order_id: str max_attempts: int errors: list[str] = field(default_factory=list, repr=False)def record_failure(plan: RetryPlan, reason: str) -> RetryPlan: plan.errors.append(reason) return planplan = RetryPlan(order_id="ORD-7", max_attempts=3)print(record_failure(plan, "timeout"))print(plan.errors)Why this design exists
PEP 557 lets records keep normal Python class behavior while generated methods remove boilerplate. Field-level configuration exists because record semantics are not one-size-fits-all: constructor parameters, repr exposure, equality cost, and hash safety may differ per field.
Mechanics and CPython internals
The decorator examines annotated fields during class creation and generates methods. ClassVar and InitVar change field treatment. slots=True asks dataclasses to return a slotted class layout. Descriptor-typed defaults participate in descriptor lookup instead of becoming ordinary literal defaults.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from dataclasses import dataclassclass PositiveInt: def __set_name__(self, owner: type[object], name: str) -> None: self.name = "_" + name def __get__(self, obj: object | None, owner: type[object] | None = None) -> object: if obj is None: return self return getattr(obj, self.name) def __set__(self, obj: object, value: int) -> None: if value < 1: raise ValueError("value must be positive") setattr(obj, self.name, value)@dataclassclass Window: width: int = PositiveInt()window = Window(5)print(window.width)Complexity and tradeoffs
Generated equality compares the fields it includes, so large mutable payload fields can make comparison expensive or semantically wrong. Slots can reduce per-instance overhead and block arbitrary new attributes, but that narrower layout affects weak references and some metaprogramming patterns. Hash generation follows safety policy rather than convenience.
Idiomatic patterns and refactoring
Refactor mutable literal defaults into factories and key objects into frozen records.
# [CURRENT - 3.10-3.14] Works on Python 3.10+from dataclasses import dataclass, field@dataclassclass Draft: labels: list[str] = field(default_factory=list)@dataclass(frozen=True, slots=True)class CacheKey: tenant: str region: strdef attach(draft: Draft, label: str) -> Draft: draft.labels.append(label) return draftprint(attach(Draft(), "retry").labels)print({CacheKey("TEN-1", "BR"): "warm"})Common mistakes and edge cases
Do not force unsafe_hash=True on mutable equality state because a dict needs a key today. Do not expose secrets in generated repr fields. Do not read __slots__ as a dataclass field registry; use dataclasses.fields.
When to use / When NOT to use
Use dataclass fields when generated method policy should be declarative and visible next to the field definitions.
Do not stretch dataclasses into full validation frameworks when an explicit constructor, descriptor, property, or separate parser makes the lifecycle clearer.