I migrated my flashcards to Mochi recently, and an old card resurfaced that I’d written maybe a year ago:

Q: What’s a huge disadvantage of using dataclasses? A: Dataclasses reduce encapsulation, code will inherently be more coupled because all variables can be accessed directly without hiding information.

I stared at it and thought: I don’t agree with this anymore.

My first reaction was — but you can do information hiding on a dataclass. Slap a @property on a private field and you’ve got encapsulation. The decorator doesn’t stop you — though in practice, properties and the auto-generated __init__ don’t mix cleanly. Which is itself a hint.

But the more I thought about it, the more I realized the card wasn’t just badly worded — it was asking the wrong question. The problem with dataclasses isn’t what they lack. It’s what happens when you use them for something they weren’t designed for.

Value Objects: where dataclasses shine

@dataclass(frozen=True)
class Point:
    x: float
    y: float
 
Point(1, 2) == Point(1, 2)  # True

A point at (1, 2) is a point at (1, 2). There’s no “which point”. The dataclass __eq__ comparing all fields is exactly the right behavior here. Same for Money, EmailAddress, DateRange — pure value carriers where identity = values.

Entities: where it breaks

@dataclass
class User:
    id: int
    name: str
    email: str
 
alice = User(id=1, name="Alice", email="[email protected]")
alice_updated = User(id=1, name="Alice", email="[email protected]")
 
alice == alice_updated  # False — but this is the same user!

Same user, email just changed. The dataclass says they’re different because it compares all fields. In any if user in existing_users check, this user would be missed.

So you override __eq__ to compare only by ID. But then you’ve thrown away the behavior the dataclass generated for you. eq=False exists as an option — but PEP 5571, the PEP that introduced dataclasses, describes them as “classes which exist primarily to store values.” Structural equality is the intended behavior. When a user complained that two empty dataclass instances compared as equal, Smith’s response was: “It’s identical to () == () returning True.”2 Setting eq=False in this example isn’t a pragmatic workaround — it’s a signal that the abstraction may not fit your use case.

The actual rule

Value Object@dataclass(frozen=True). Perfect fit. Entity → plain class. The convenience isn’t worth the semantic mismatch.

The flashcard’s answer — “reduces encapsulation” — misses the point. Exposing fields isn’t the problem. That’s what a value store is supposed to do. The problem is reaching for a value store when what you need is an object with identity.

I rewrote the flashcard.

Q: Python @dataclass: When should you NOT use a dataclass? A: For Entities — objects with identity independent of their attributes. A User who changes email is still the same User, but dataclass eq compares all fields and says they’re different. Use a plain class with ID-based equality instead.

Footnotes

  1. PEP 557 — Data Classes

  2. bugs.python.org #46739 — “dataclasses eq isn’t logical”