You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.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)
Summary
2.0 will remove
HumanName.__eq__andHumanName.__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()andcomparison_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 comparesstr(self).lower()againststr(other).lower(), and__hash__(added in 1.2.0, #138) hashes that string. This makes three promises:hn == "John Smith"→ True)These are mathematically incompatible. If
hn == "John Smith"andhn == "john smith"are both True, the hash invariant (a == b⇒hash(a) == hash(b)) requireshash(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
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.)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/groupbyMigration (from 1.3.0's deprecated
==)hn == xhn.matches(x)"john smith" == hn(reflected)hn.matches("john smith")— must flip operandshn == maybe_nonemaybe_none is not None and hn.matches(maybe_none)—matches()raisesTypeErroron non-str/HumanNamehn in names,.index(),assertInany(hn.matches(n) for n in names)orcomparison_key()setshash(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
__eq__and__hash__fromHumanNamedocs/usage.rstdeprecation note (the equality example already usesmatches()as of 1.3.0)