Verified against Claude Code · 2026-07-31
Convert a hand-rolled class to a dataclass without changing its equality semantics by accident
A prompt for converting a manually-written __init__/__eq__/__repr__ class into a dataclass (or attrs class) with the right frozen, eq, and hashability settings for how instances are actually used, instead of a default dataclass conversion that silently breaks set or dict membership.
The prompt
Ready to copy — highlighted parts are example details you can swap.
Convert the class below to a dataclass (or attrs, if you name why attrs fits better here). This must preserve exactly how instances are currently compared, hashed, and mutated — a dataclass conversion that changes equality or hashability semantics without saying so is a correctness bug, not a style improvement. CLASS class Point:\n def __init__(self, x, y):\n self.x = x\n self.y = y\n # no __eq__ defined HOW INSTANCES ARE USED Instances are stored in a set() to dedupe visited coordinates during a pathfinding search, and never mutated after creation. MUTABILITY Immutable — a Point is created once and never reassigned; a "moved" point is a new Point instance. REQUIREMENTS 1. Before converting anything, state what the current class's __eq__, __hash__, and __repr__ actually do today — if it doesn't define __eq__, say explicitly that it currently uses identity comparison (is), because @dataclass generates a field-by-field __eq__ by default, which is a real behavior change if the code anywhere currently relies on identity. 2. Set frozen= based on Immutable — a Point is created once and never reassigned; a "moved" point is a new Point instance., not by default. If instances must be usable as dict keys or set members, they need to be hashable, which requires either frozen=True (dataclass then auto-generates a compatible __hash__) or an explicit eq=False with a hand-written __hash__ — pick correctly and say why, don't leave a mutable dataclass with eq=True, which is unhashable by default and will raise TypeError the first time someone tries to put an instance in a set. 3. Use field(default_factory=...) for any mutable default (a list, dict, or set attribute) — never a bare mutable default value, which dataclass itself explicitly forbids at class-definition time and will raise ValueError if attempted, unlike a plain class where the same mistake fails silently. 4. If the original class has any custom method beyond __init__/__eq__/__repr__/__hash__ (a computed property, a validation method, a classmethod constructor), keep it as-is on the dataclass — a dataclass is still a normal class, and business logic doesn't disappear just because field boilerplate did. 5. If Instances are stored in a set() to dedupe visited coordinates during a pathfinding search, and never mutated after creation. shows instances being mutated after construction in a way that's incompatible with the mutability decision in step 2, flag the conflict explicitly rather than silently picking one side. 6. If Subclassed once, by GridPoint(Point), which adds a "layer" field with no default of its own. shows this class is subclassed elsewhere, check field ordering carefully: a dataclass field with a default value cannot be followed by a subclass field without one, so converting a base class to a dataclass can break an existing subclass's field ordering in a way that only surfaces as a TypeError at class-definition time in a completely different file. 7. Consider whether slots=True is worth adding given how many instances Instances are stored in a set() to dedupe visited coordinates during a pathfinding search, and never mutated after creation. implies will exist at once — it removes each instance's __dict__ in favor of fixed storage, which matters for memory at scale but breaks dynamic attribute assignment, so state explicitly whether that trade-off fits here rather than defaulting to it or skipping it without comment. OUTPUT FORMAT 1. What the original class's equality/hash/repr behavior actually was. 2. The converted dataclass. 3. One sentence confirming whether instances remain usable exactly where Instances are stored in a set() to dedupe visited coordinates during a pathfinding search, and never mutated after creation. needs them (as dict keys, in a set, appended to a list and mutated later, etc.), or naming the specific conflict if one exists. 4. One sentence on whether Subclassed once, by GridPoint(Point), which adds a "layer" field with no default of its own. introduces any field-ordering risk, and whether slots=True was worth adding.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
Requiring the model to state the original class's current equality behavior before converting anything catches the single most consequential silent behavior change in this whole class of refactor: a hand-written class with no __eq__ defined uses identity comparison by default, but @dataclass generates a field-by-field __eq__ automatically unless told not to, so a naive conversion changes what "equal" means for every existing comparison, every set membership check, and every dict lookup keyed on instances of this class, without a single line of new code visibly signaling that anything changed. The frozen= decision tied explicitly to {{mutability_requirement}} rather than left as a default matters because Python's actual rule here is unforgiving and easy to get backwards: a dataclass with the default eq=True and frozen=False (the plain @dataclass with no arguments) is unhashable, full stop, and the first time code tries to put an instance in a set or use it as a dict key it raises TypeError: unhashable type at that call site, far from wherever the dataclass itself was defined, making the actual root cause hard to trace back. The field(default_factory=...) requirement for mutable defaults isn't just a best practice suggestion here — dataclass enforces it at the language level, raising ValueError the instant a bare mutable default like a list literal is used as a field default, which is strictly better than a plain class's equivalent mistake (a mutable default argument on __init__) that fails completely silently and only manifests as a confusing bug much later when state leaks between instances that were never supposed to share anything. The subclassing_context field catches a dataclass-specific rule that has no equivalent failure mode in a hand-written class: dataclass generates its __init__ by walking fields in declaration order across the whole inheritance chain, and a field with a default value cannot be followed by one without a default anywhere in that chain, so converting a previously plain base class to a dataclass can silently turn a perfectly fine existing subclass into a TypeError at import time, in a file that wasn't touched by this refactor at all and that nobody would think to check.
What you get back
# Original: no __eq__ defined -> uses identity (is) comparison by default. @dataclass(frozen=True) class Point: x: float y: float # frozen=True chosen because usage_pattern stores instances in a set() and never mutates them; # frozen=True gives a compatible auto-generated __hash__ for free. Confirmation: instances remain hashable and usable in a set() exactly as usage_pattern requires. Note the equality semantics changed from identity to field-based value equality — flagged because any code relying on "is" comparison elsewhere would now behave differently and should be checked.
Verified against
Claude Code Sonnet 4.6 · 2026-07-31
Cursor 2.1 · 2026-08-01
Changelog
- 2026-08-01 — Initial publish, verified against Claude Code (Sonnet 4.6) and Cursor 2.1 on Python 3.12.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
