Repository navigation
Remove __eq__ and __hash__ in 2.0; compare names via explicit methods #223
Description
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:
- Case-insensitive equality
- Cross-type equality with plain strings (
hn == "John Smith"→ True) - Hashability (usable in sets/dicts)
These are mathematically incompatible. If hn == "John Smith" and hn == "john smith" are both True, the hash invariant (a == b ⇒ hash(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.
HumanNameis 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 throughstring_format, so changingCONSTANTS.string_formatretroactively changes whether two existing objects are equal. Same class of shared-mutable-state problem as Stop parsing from mutating the Constants it reads #222. maidenis invisible to equality — the defaultstring_formathas no{maiden}placeholder.hn == 42can be True (if the name is "42");__eq__stringifies any operand instead of returningNotImplemented.
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 componentshn.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__fromHumanName - Replace equality/hash tests with identity-semantics tests
- Update the
docs/usage.rstdeprecation note (the equality example already usesmatches()as of 1.3.0) - Migration section in docs + release log entry
Activity
Reconcile test_python_api against the facade (buckets A, B, E)
docs: rewrite the 2.0 release log new-API-first and complete the audit
derek73 commentedon Jul 28, 2026
Shipped in 2.0.0. Delivery and release-log coverage were verified issue-by-issue in the pre-release milestone audit.