This section is about two small standard-library tools that make data-shaped classes pleasant to write: dataclasses and enum. The first one is @dataclass, a decorator that takes a class with type-annotated attributes and writes the boring methods for you: __init__, __repr__, and __eq__. This chapter covers the decorator at its plainest, the kind of class you'd reach for to model a Product, a Customer, or an Order line.
A lot of classes in real code are essentially containers: a name, a price, a stock count, maybe an is_active flag. The behavior is "hold those values, print nicely, compare by content." Writing that by hand is repetitive. Here's the plain-class version of a Product:
Three attributes, fifteen lines, and three of those methods are pure boilerplate. The constructor just copies parameters onto self. The repr restates every field. The equality method compares every field. None of that code says anything interesting about a product; it's the same shape every class with three fields would have.
The @dataclass decorator, added in Python 3.7, looks at a class with type-annotated attributes and writes those methods for you. Same class:
That's the whole class. The decorator inspects the class body, finds three annotated attributes, and generates __init__, __repr__, and __eq__ with the same behavior as the hand-written version. The fields you list in the class body become the parameters to __init__, in order.
The class is still a normal Python class. You can add methods, inherit from it, and use it anywhere a regular class fits. The decorator just removes the typing you'd otherwise repeat.
Let's build one and use it:
A few things to notice in that output. Creating an instance with Product("Running Shoes", 79.99, 12) worked because the decorator built an __init__ that takes the three fields in the order they appear in the class body. Printing the object gave a useful, multi-field representation instead of <__main__.Product object at 0x10a3b4c10>, because the decorator also generated __repr__. The attributes are accessible on the instance the same way they would be on any other class.
Keyword arguments work too, and they tend to read better when the order isn't obvious:
The fields are positional by default, so positional arguments work in declaration order, and keyword arguments work by name. That's the same rule any normal function follows, because __init__ is just a normal function the decorator wrote for you.
It's worth knowing exactly what the decorator gives you, because every dataclass in the rest of this section builds on these three methods.
| Method generated | What it does |
|---|---|
__init__(self, ...) | Takes one parameter per field, in declaration order, and assigns each to self |
__repr__(self) | Returns ClassName(field1=value1, field2=value2, ...) for printing and debugging |
__eq__(self, other) | Returns True when other is the same class and every field compares equal |
Equality is the part people are most surprised by. Two instances of a plain class are unequal by default, even when every field has the same value. With @dataclass, equality compares by content:
a and b are different objects in memory (so a is b is False), but they have identical field values, so the generated __eq__ returns True. Change one field and equality breaks. This is what most code wants for value-shaped types: two orders with identical contents should compare equal.
The generated __repr__ is the other quality-of-life win. Without it, printing an object gives you a useless memory-address line. With it, you see the actual contents:
Same data, very different debugging experience. The plain class would need a hand-written __repr__ to get parity, which is exactly the boilerplate @dataclass removes.
Here's the rule that trips people up first: @dataclass only treats an attribute as a field when it has a type annotation. The annotation can be anything (str, int, float, list, even object), but it has to be there.
This works:
This one doesn't behave the way you'd expect:
No order_id, no total, and Order() takes no arguments. The decorator ignored both lines because neither had a type annotation, so it found zero fields and generated an __init__ with no parameters. The attributes are still on the class as class-level constants, but they're not part of the instance contract.
Try to pass arguments and you get a clear error:
The fix is to add annotations:
The annotations don't enforce types at runtime. You can still pass Order(123, "free") and Python won't object; the decorator uses the annotations as a list of fields, not as a runtime type check. Static type checkers like mypy and pyright use them, and editors use them for autocomplete, but @dataclass itself treats them as field declarations.
A field can have a default value the same way a function parameter can: write it after the type annotation with =.
Defaults follow the same rule as normal Python function parameters: every field with a default has to come after every field without one. The decorator builds __init__ in declaration order, and Python doesn't allow a non-default parameter after a default. If you put stock: int = 0 before price: float, the decorator raises an error at class-definition time:
That error fires when Python tries to define the class, not when you create an instance. Move every defaulted field to the bottom and the class compiles cleanly.
Defaults only work this way for simple immutable values like numbers, strings, booleans, and None. Mutable defaults like [], {}, or set() are a separate topic with their own gotcha, because writing items: list = [] shares one list across every instance. The fix uses field(default_factory=list). For this chapter, stick to simple immutable defaults.
Cost: @dataclass does its work once, at class-definition time. It inspects the annotations, generates source code for the methods, compiles them, and attaches them. After that, creating instances is the same speed as a plain class. There's no per-instance overhead.
The generated __eq__ compares two instances field by field, in declaration order. Two instances are equal when they're the same class and every field is equal.
That's enough to support a lot of useful patterns. Checking whether two carts contain the same line items, deduplicating a list of orders with set won't work yet (we need hashability for that, and the default dataclass isn't hashable), but plain == comparison and in checks for lists already work:
The in check uses __eq__ under the hood. Because the dataclass equality compares contents, you can ask "is this exact item already in the cart?" without writing a loop. Without @dataclass, that check would fail because plain-class == falls back to identity comparison.
Equality is also class-aware. Two dataclasses with the same field names and values but different classes are not equal:
The generated __eq__ checks the type first. A cart item and a wishlist item with the same contents are still different things, so equality returns False. That's the behavior almost every value class wants.
What @dataclass does not generate by default is ordering (<, <=, >, >=). Trying to sort or compare order is a TypeError:
Ordering is opt-in via @dataclass(order=True), which is one of the decorator options alongside frozen=True and slots=True. For now, the rule is: == and != work out of the box, ordering does not.
10 quizzes