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) # TrueA 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.