skip to content

What does the @dataclass decorator generate, and which class attributes become fields?

level: juniorimportance: must knowfreq 72%

answer

  1. One decorator writes methods for you
  2. It runs at class-creation time
  3. Something in the body must be present
  4. The colon, not the equals sign
  5. init, repr and eq — and nothing else

basics

~10 s

@dataclass writes init, repr and eq from the class's annotated attributes, in declaration order. Only annotated names become fields; a plain assignment with no annotation stays an ordinary class attribute.

solid answer

~40 s

`dataclasses.dataclass` runs once, when the class is created. It reads the class's annotations in order, builds a field for each annotated name, then generates and attaches `__init__`, `__repr__` and `__eq__`. The generated `__init__` takes one parameter per field in that order, using the assigned value as the parameter default. `__repr__` prints `ClassName(field=value, ...)`, and `__eq__` compares the tuple of fields but only against an instance of the same class — otherwise it returns `NotImplemented`. The switch that makes something a field is the **annotation**, not the assignment: `grade = "junior"` in the body is a plain class attribute and never reaches `__init__`, while `typing.ClassVar[...]` is the explicit way to say "class-level constant, not a field". The annotation itself is never enforced at runtime — passing a `str` where `int` is annotated constructs fine.

code

python · 12 lines
python
from dataclasses import dataclass

@dataclass
class Employee:
    name: str
    hours: float = 0.0
    grade = "junior"          # no annotation -> not a field

e = Employee("Ada", 37.5)
print(e)                      # Employee(name='Ada', hours=37.5)
print(e == Employee("Ada", 37.5))   # True
print("grade" in e.__dict__)  # False - it stayed a class attribute

go deeper

for a junior

Be ready to name the three generated methods and to say that a field is created by the annotation, not by the assignment. Knowing that x = 5 in the body is not a field is the whole screening question.

for a middle

Explain the mechanics: the decorator runs once at class creation, reads annotations in order, and that order becomes the positional signature of __init__. Be able to say why ClassVar is excluded and why annotations are not enforced.

for a senior

Show judgement about the generated __eq__ — same-class-only comparison, NotImplemented for anything else — and about field order being a public contract that positional call sites depend on.

for a principal

Own the boundary question: when a bundle of named values should be a dataclass versus a hand-written class with real construction logic, and how a team keeps annotation-only 'types' from being mistaken for validated invariants.

## What the decorator actually does `dataclasses.dataclass` is an ordinary decorator, not compiler magic. It runs exactly once — when the `class` statement finishes executing — and it does three things: it collects the class's annotations, turns each one into a field descriptor, and then generates source text for the methods you asked for, compiles it, and attaches the resulting functions to the class. After that the class is a normal class; instances pay no per-instance cost for having been generated this way, and you can still add methods, properties and inheritance as usual. `@dataclass` and `@dataclass()` are equivalent. ## The annotation is the switch This is the part interviewers actually probe: ```python from dataclasses import dataclass @dataclass class Employee: name: str hours: float = 0.0 grade = "junior" # no annotation -> NOT a field ``` `name` and `hours` are fields. `grade` is not: it never appears in the generated `__init__`, `__repr__` or `__eq__`, and it stays a single class-level object shared by every instance. Nothing warns you — the class builds and runs, and the missing constructor parameter shows up later as a confusing `TypeError` at a call site. The mirror-image mistake *is* caught: assigning `dataclasses.field(...)` to a name with no annotation raises `TypeError: 'x' is a field but has no type annotation`. When you genuinely want a class-level constant, annotate it `typing.ClassVar[...]`. `@dataclass` special-cases that annotation and leaves the name out of the field list entirely, which documents the intent instead of relying on a missing colon. ## The three generated methods **`__init__`** takes one parameter per field, in annotation order, and the value assigned in the class body becomes that parameter's default. A field with no assignment has no default — internally its default is the sentinel `dataclasses.MISSING` — and its argument is required. **`__repr__`** produces `Employee(name='Ada', hours=37.5)`: the class name plus each field as `name=repr(value)`. It is the eval-shaped debugging repr, which is most of why people reach for a dataclass in the first place. **`__eq__`** compares the two objects field-by-field, as if comparing tuples of their field values — but only when the other object's class is exactly the same class. Against anything else it returns `NotImplemented`, so Python falls back and the comparison ends up `False`. Two structurally identical dataclasses of *different* classes are therefore never equal, which is usually what you want for value types. Only those three are generated by default. Ordering methods, immutability and keyword-only construction are separate opt-ins. ## Annotations are not validation The annotation's value is metadata. `Employee("Ada", "many")` constructs happily and stores the string in `hours`; nothing checks it. A static type checker flags it, the interpreter does not. If a field needs a real invariant, you have to write that check yourself. Candidates who claim "dataclasses give you runtime type checking" are stating the single most common misconception about them. ## Order matters, and it is the annotation order Field order is the order the annotations appear in the class body, and that order becomes the positional signature of `__init__` and the field order in `__repr__` and `__eq__`. Reordering two annotations is therefore a breaking change to every positional call site — the same hazard as reordering function parameters. Inherited fields come first, in base-class order, before the ones the subclass adds. ## Version notes `dataclasses` has been in the standard library since Python 3.7. Python 3.14 changed *when* annotations are evaluated — PEP 649 made them lazy, evaluated on demand rather than at class-body execution — but `@dataclass` still consults the class's annotations while the decorator is running, so which names are fields, and in what order, is unchanged. One practical effect of the 3.14 model is that forward references in annotations are far less likely to need quoting. ## When this is the wrong tool A dataclass instance is a mutable object with named attributes. It is not iterable, not indexable and not unpackable like a tuple-shaped record, and it is not a serialization or validation framework. Reach for it when you want a small, readable, comparable bundle of named values with a useful `repr` — and write the class by hand when the construction logic is genuinely interesting.

  • Does a dataclass check that a value matches its field's annotation at construction time?
    No. The annotation is metadata; the generated `__init__` just assigns whatever you pass. `Employee("Ada", "many")` stores the string in a field annotated `float` without complaint. A static type checker catches it before the code runs, and a runtime invariant has to be written by hand. Third-party validation libraries add that layer on top; the stdlib decorator deliberately does not.
  • Why does the generated __eq__ return NotImplemented instead of False for a different class?
    Returning `NotImplemented` tells Python "I don't know how to compare these", so the interpreter tries the other operand's reflected `__eq__` before falling back to identity comparison. That lets an unrelated class opt into equality with your dataclass. Hard-coding `False` would silently defeat that. The generated method requires `other.__class__` to be exactly the same class, so structurally identical dataclasses of different classes never compare equal.
  • How do you keep a class-level constant out of the generated __init__?
    Annotate it with `typing.ClassVar[...]`. `@dataclass` recognises that annotation and excludes the name from the field list, so it stays a shared class attribute and never becomes a constructor parameter. Simply leaving the annotation off also works, but it reads like a typo and, if you assign `dataclasses.field(...)` to an unannotated name, raises `TypeError: ... is a field but has no type annotation`.

saying these in an interview costs you the question

  • Claiming dataclasses validate field types at runtime
  • Saying any class-body assignment becomes a field
  • Thinking the decorator runs per instance
  • Expecting a dataclass to be unpackable like a tuple
  • Believing comparison methods like __lt__ are generated by default

context