Python Dataclasses

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__, like self.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)
True

It 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
@dataclass
class 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)
True

Same behaviour, far less code. Let us go through what the decorator did.

  • x: int and y: int are the fields. The : int part is a type hint that says each one holds a whole number.
  • Python read those fields and wrote __init__ for you. So Point(2, 3) sets self.x and self.y on its own.
  • It wrote __repr__ too. That is why print(p1) shows the clean Point(x=2, y=3) instead of something like a memory address.
  • It wrote __eq__ as well. That is why p1 == Point(2, 3) is True, 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
@dataclass
class 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

True
False
False

Here is what each line says:

  • a == b is True because both hold (1, 5). The generated __eq__ compares the values.
  • a == c is False because (1, 5) does not match (9, 9).
  • a is b is False because is asks 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
@dataclass
class 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
@dataclass
class 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.0
True

So 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 = 0 line 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
@dataclass
class Point:
x = 0
# βœ… Good: type hint makes it a field
@dataclass
class 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
@dataclass
class Product:
in_stock: bool = True
name: str
# βœ… Good: required fields first, defaulted fields after
@dataclass
class Product:
name: str
in_stock: bool = True
  • Expecting is to behave like ==. The dataclass changes == only. is still 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. Use field(default_factory=list) from the dataclasses module 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 dataclass and the @dataclass decorator.
  • βœ… Let Python auto-generate __init__, __repr__, and __eq__ for you.
  • βœ… Compare two dataclass objects and know that == checks the data while is checks 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. 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. 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. 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. 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.

Type Hints

Share & Connect