Agent Wranglers

ARC-71Recommended

Keep equality and hashing in step

When a type defines its own equality, it defines a hash from the same fields. Two equal objects always have the same hash, and the fields in the hash never change.

How to do it

Hash maps and sets find an item by its hash first. Equal objects with different hashes then get lost or stored twice. Build both from the same fields, and make those fields read-only, as Make data objects immutable says. The easiest way is a generated type, such as a Python frozen dataclass, a Java or C# record or a Kotlin data class. In Python, a class that defines __eq__ alone cannot go in a set at all.

Examples

Python example

# Bad: equal SKUs, but a set holds both, and dict lookups miss
class Sku:
    def __init__(self, code: str): self.code = code
    def __eq__(self, other): return self.code == other.code
    def __hash__(self): return id(self)

# Good: equality and hash come from the same read-only field
@dataclass(frozen=True)
class Sku:
    code: str

Java example

// Good: a record generates equals and hashCode from the same fields
public record Sku(String code) {}

How to check

A linter fails a class that overrides one method without the other. Examples are SpotBugs HE_EQUALS_NO_HASHCODE for Java, the C# warning CS0659 and Ruff PLW1641 for Python. A test puts two equal objects in a set and expects its size to be 1.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/architecture/data-shapes/equality-and-hash">Agent Wranglers architecture playbook</a>. In code you write, add a comment next to it with the same link.