Python Dataclasses
Table of Contents + β
In the last lesson you learned Custom Context Managers. There you wrote your own setup-and-cleanup blocks. Now we move to a different kind of class. This is the kind that mostly just holds data. Python has a neat tool that writes most of that class for you.
π€ Why Dataclasses?
Say you want a small class to hold a point on a map. It has an x and a y. That is all. But to make it behave nicely, you end up writing a lot of repeated code.
Here is the pain. You write a class to hold data. Then you find yourself typing the same patterns again and again.
- You assign every field by hand inside
__init__, likeself.x = x. - You write a
__repr__so printing the object shows something readable. - You write an
__eq__so two objects with the same data count as equal.
That is a lot of boilerplate for a class that just holds a few values. Boilerplate means repeated setup code that you have to write but that carries no real idea of its own.
A dataclass fixes this. You describe the fields once. Then Python writes that repeated code for you.
π§© What Is a Dataclass?
A dataclass is a normal class with a helper attached. You add the @dataclass decorator above it. You list the fields you want. Then Python fills in the common methods.
Think of it like ordering furniture that comes pre-built. A plain class is the flat-pack box. You screw every leg on yourself. A dataclass is the same table delivered already put together. You still own a table. You just skipped the assembly.
It comes from the standard library, so there is nothing to install.
from dataclasses import dataclassπ§ The Old Way vs the Dataclass Way
Let us write the same Point class twice. First the long way, by hand.
This is the plain class, where you write every method yourself:
class Point: def __init__(self, x, y): self.x = x self.y = y
def __repr__(self): return f"Point(x={self.x}, y={self.y})"
def __eq__(self, other): return self.x == other.x and self.y == other.y
p1 = Point(2, 3)print(p1)print(p1 == Point(2, 3))When you run it, you get this:
Output
Point(x=2, y=3)TrueIt works. But look how much typing that took for a class that only holds two numbers.
Now the same thing as a dataclass. We add the decorator and just name the fields:
from dataclasses import dataclass
@dataclassclass Point: x: int y: int
p1 = Point(2, 3)print(p1)print(p1 == Point(2, 3))When you run it, you get the exact same result:
Output
Point(x=2, y=3)TrueSame behaviour, far less code. Let us go through what the decorator did.
x: intandy: intare the fields. The: intpart is a type hint that says each one holds a whole number.- Python read those fields and wrote
__init__for you. SoPoint(2, 3)setsself.xandself.yon its own. - It wrote
__repr__too. That is whyprint(p1)shows the cleanPoint(x=2, y=3)instead of something like a memory address. - It wrote
__eq__as well. That is whyp1 == Point(2, 3)isTrue, even though they are two separate objects.
Note
The fields need a type hint to be picked up. If you are not sure of the type, you can write x: object or use a real hint like int, str, or float. The hint is also documentation, so prefer a real one.
π° Equality Comes for Free
This part surprises people coming from other languages. With a plain class, two objects are equal only if they are literally the same object in memory. With a dataclass, two objects are equal when their data matches.
Here we make two separate Point objects that hold the same numbers and compare them:
from dataclasses import dataclass
@dataclassclass Point: x: int y: int
a = Point(1, 5)b = Point(1, 5)c = Point(9, 9)
print(a == b)print(a == c)print(a is b)Running this gives:
Output
TrueFalseFalseHere is what each line says:
a == bisTruebecause both hold(1, 5). The generated__eq__compares the values.a == cisFalsebecause(1, 5)does not match(9, 9).a is bisFalsebecauseisasks a different question. It asks βare these the exact same object?β, and they are two different objects.
So == checks the data, and is checks identity. The dataclass only gives you the data check.
π Default Values for Fields
You can give a field a default, just like a function argument. Then callers can leave it out.
Here a Product has a name and a price, and an in_stock flag that defaults to True:
from dataclasses import dataclass
@dataclassclass Product: name: str price: float in_stock: bool = True
phone = Product("Pixel Phone", 699.0)poster = Product("Wall Poster", 12.5, in_stock=False)
print(phone)print(poster)Running it shows the default filling in:
Output
Product(name='Pixel Phone', price=699.0, in_stock=True)Product(name='Wall Poster', price=12.5, in_stock=False)The phone never set in_stock, so it took the default True. The poster passed in_stock=False, so it used that instead. Same rule as default arguments in a normal function.
Caution
Fields with a default must come after fields without one. If you put a default field before a required one, Python raises a TypeError when the class is defined. This is the same ordering rule as default arguments in a function.
ποΈ A Fuller Real Example
Dataclasses still hold normal methods. You add behaviour the same way you would in any class. Here is a Product that can tell you its price after a discount.
We define the data fields, then a regular method that uses them:
from dataclasses import dataclass
@dataclassclass Product: name: str price: float in_stock: bool = True
def price_after(self, discount): return round(self.price * (1 - discount), 2)
laptop = Product("Travel Laptop", 1000.0)
print(laptop)print(laptop.price_after(0.20))print(laptop == Product("Travel Laptop", 1000.0))Running this gives:
Output
Product(name='Travel Laptop', price=1000.0, in_stock=True)800.0TrueSo you got the clean printout, a working method you wrote, and free equality. All of that came from one decorator and a few field lines. The method price_after is just a normal method. The decorator does not touch it.
π What the Decorator Writes for You
Here is a quick view of what you skip writing when you reach for a dataclass.
| Method | What it does | Who writes it |
|---|---|---|
__init__ | Sets each field when you create the object | The dataclass |
__repr__ | Gives a readable text form for printing | The dataclass |
__eq__ | Compares two objects by their field values | The dataclass |
| Your own methods | Any behaviour you add, like price_after | You |
β οΈ Common Mistakes
A few things trip people up the first time they use dataclasses.
- Forgetting the type hint on a field. A plain
x = 0line is treated as a class variable, not a field. So it never reaches__init__.
# β Avoid: no type hint, so x is not a real field@dataclassclass Point: x = 0
# β
Good: type hint makes it a field@dataclassclass Point: x: int = 0- Putting a default field before a required one. Python reads them in order and complains.
# β Avoid: default field before a required one raises TypeError@dataclassclass Product: in_stock: bool = True name: str
# β
Good: required fields first, defaulted fields after@dataclassclass Product: name: str in_stock: bool = True-
Expecting
isto behave like==. The dataclass changes==only.isstill asks whether they are the exact same object, which is rarely what you want here. -
Using a mutable default like a list directly. Writing
tags: list = []shares one list across every object. Usefield(default_factory=list)from thedataclassesmodule instead.
β Best Practices
Small habits that keep dataclasses clean:
- Reach for a dataclass whenever a class is mostly fields with little behaviour. That is exactly what it is built for.
- Give every field a real, meaningful type hint. It documents the class and makes the field count.
- Order fields so required ones come first and defaulted ones come last.
- For a default that is a list, dict, or set, use
field(default_factory=...)rather than a bare[]or{}. - Keep your own methods on the dataclass when they belong to that data, like
price_after. The decorator leaves them alone.
π§© What Youβve Learned
A quick recap of what you can now do:
- β Explain what boilerplate is and why data-holding classes have so much of it.
- β
Turn a plain class into a dataclass with
from dataclasses import dataclassand the@dataclassdecorator. - β
Let Python auto-generate
__init__,__repr__, and__eq__for you. - β
Compare two dataclass objects and know that
==checks the data whileischecks identity. - β Give fields default values and keep required fields before defaulted ones.
- β Add your own methods to a dataclass without losing any of the free features.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
What does the @dataclass decorator auto-generate for you?
Why: The @dataclass decorator reads your typed fields and writes __init__, __repr__, and __eq__ automatically.
- 2
Why does a dataclass field need a type hint like x: int?
Why: Only annotated names are treated as fields; a plain x = 0 without a hint is just a class variable and never reaches __init__.
- 3
Given a = Point(1, 5) and b = Point(1, 5), what are a == b and a is b?
Why: The generated __eq__ makes == compare the data (True), but they are still two separate objects, so is is False.
- 4
Where must a field with a default value go in a dataclass?
Why: Just like default function arguments, fields with defaults must come after fields without them, or Python raises a TypeError.
π Whatβs Next?
You can now build clean data-holding classes without the busywork. Next we look at how those type hints on the fields fit into a bigger idea across all your code.