Skip to content

Remove __eq__ and __hash__ in 2.0; compare names via explicit methods #223

Description

@derek73

Summary

2.0 will remove HumanName.__eq__ and HumanName.__hash__. Instances revert to Python's default identity semantics (still safely usable in sets/dicts — see below). Value comparison moves to two explicit methods, shipped in 1.3.0 so the APIs overlap for a full minor release: matches() and comparison_key() (added in PR #247).

Feedback welcome — this is a breaking change and the rationale is laid out in full below. The deprecation warnings shipped in 1.3.0 (#224, implemented in PR #247; released).

Why the current design can't be fixed in place

__eq__ currently compares str(self).lower() against str(other).lower(), and __hash__ (added in 1.2.0, #138) hashes that string. This makes three promises:

  1. Case-insensitive equality
  2. Cross-type equality with plain strings (hn == "John Smith" → True)
  3. Hashability (usable in sets/dicts)

These are mathematically incompatible. If hn == "John Smith" and hn == "john smith" are both True, the hash invariant (a == bhash(a) == hash(b)) requires hash(hn) to equal two different values. Consequence today: {hn} | {"John Smith"} is a two-element set whose elements compare equal — broken dedup, the very use case #138 added hashing for. No implementation can fix this; one of the three promises has to go.

Additional defects in the current design

  • Mutation strands container entries. HumanName is mutable; assigning any attribute changes the hash, so an instance already in a set/dict becomes unfindable. (Python nulls __hash__ when a class defines __eq__ for exactly this reason — Make HumanName objects hashable #138 re-enabled what the language had disabled.)
  • Equality depends on display config. str(self) goes through string_format, so changing CONSTANTS.string_format retroactively changes whether two existing objects are equal. Same class of shared-mutable-state problem as Stop parsing from mutating the Constants it reads #222.
  • maiden is invisible to equality — the default string_format has no {maiden} placeholder.
  • hn == 42 can be True (if the name is "42"); __eq__ stringifies any operand instead of returning NotImplemented.

Why removal rather than component-based __eq__/__hash__

A component-based == was considered and is workable, but it still privileges one equality semantic for a domain that has none (is "J. Smith" equal to "John Smith"? "Smith, John"? every application answers differently), and still carries the mutate-while-hashed caveat. Removing both dunders is the only option with no footguns left: default identity hash is always stable and always consistent with identity equality, so sets/dicts of HumanName keep working — they just mean "these objects" rather than "these name values". Value semantics become explicit:

  • hn.matches(other) — parses a string operand, compares parsed components
  • hn.comparison_key() — hashable tuple of components, for dedup/dicts/groupby

Migration (from 1.3.0's deprecated ==)

Before After
hn == x hn.matches(x)
"john smith" == hn (reflected) hn.matches("john smith") — must flip operands
hn == maybe_none maybe_none is not None and hn.matches(maybe_none)matches() raises TypeError on non-str/HumanName
hn in names, .index(), assertIn any(hn.matches(n) for n in names) or comparison_key() sets
set/dict dedup via hash(hn) {n.comparison_key(): n for n in names}.values()

Users with a lossy custom string_format (e.g. "{first} {last}") currently get projection equality; matches() compares all components and is stricter. Compare the specific attributes instead.

Checklist

  • Remove __eq__ and __hash__ from HumanName
  • Replace equality/hash tests with identity-semantics tests
  • Update the docs/usage.rst deprecation note (the equality example already uses matches() as of 1.3.0)
  • Migration section in docs + release log entry

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions