Summary
Signature.__hash__ and Method.__hash__ (which includes self.signature) are inconsistent with Signature.__eq__: two signatures that compare equal via __eq__ (because beartype.door.TypeHint treats their types as semantically equivalent) do not necessarily hash equal. This violates Python's hash invariant ("equal objects must hash equal") and can silently break Signature/Method-keyed hashed collections (duplicate entries in a set, unreachable keys in a dict, etc.).
Repro (unmodified master, stable beartype==0.22.9 — nothing to do with #295/#296)
from typing import Union
from plum import Signature as Sig
s1 = Sig(Union[int, bool])
s2 = Sig(int)
print(s1 == s2) # True (existing, intentional: test_signature.py::test_equality
# explicitly asserts this "equivalent but not identical" case)
print(hash(s1) == hash(s2)) # False -- hash/eq contract violated
Root cause
Signature.__eq__ compares types via beartype.door.TypeHint(x) == TypeHint(y) (through _type_hints_equal as of #296), which is a genuine semantic-equivalence check — e.g. TypeHint(Union[int, bool]) == TypeHint(int) is True. But Signature.__hash__ hashes the raw underlying hints directly:
def __hash__(self) -> int:
return hash((Signature, *self.types, self.varargs))
This isn't just a plum-side oversight, though — it's inherited straight from beartype.door.TypeHint itself, whose own __hash__ is:
def __hash__(self) -> int:
return hash(self._hint)
i.e. beartype.door.TypeHint has the exact same violation: TypeHint(Union[int, bool]) == TypeHint(int) is True while hash(TypeHint(Union[int, bool])) != hash(TypeHint(int)). Routing Signature.__hash__ through TypeHintWrapper(...) instead of the raw hint wouldn't fix anything, since it reduces to the identical hash(self._hint) call.
Why I'm not attempting a fix here
A correct fix needs a canonical form to hash semantically-equivalent-but-structurally-different hints (like Union[int, bool] and int) to the same value — that's a much bigger problem than a local tweak, and arguably belongs upstream in beartype.door.TypeHint (or requires plum to maintain its own hint-canonicalization, independent of beartype). Flagging this as its own issue rather than folding a speculative fix into an unrelated PR.
Context
Surfaced during review of #296 (unrelated Any-vs-concrete-type fix) — see this comment. That PR doesn't introduce or worsen this: it's reproducible on unmodified master with stable beartype, and for the specific Any case #296 actually removes one instance of the same violation (Signature(Any) == Signature(int) was True under beartype>=0.23 before #296's fix, while their hashes still differed).
Summary
Signature.__hash__andMethod.__hash__(which includesself.signature) are inconsistent withSignature.__eq__: two signatures that compare equal via__eq__(becausebeartype.door.TypeHinttreats their types as semantically equivalent) do not necessarily hash equal. This violates Python's hash invariant ("equal objects must hash equal") and can silently breakSignature/Method-keyed hashed collections (duplicate entries in aset, unreachable keys in adict, etc.).Repro (unmodified
master, stablebeartype==0.22.9— nothing to do with #295/#296)Root cause
Signature.__eq__compares types viabeartype.door.TypeHint(x) == TypeHint(y)(through_type_hints_equalas of #296), which is a genuine semantic-equivalence check — e.g.TypeHint(Union[int, bool]) == TypeHint(int)isTrue. ButSignature.__hash__hashes the raw underlying hints directly:This isn't just a
plum-side oversight, though — it's inherited straight frombeartype.door.TypeHintitself, whose own__hash__is:i.e.
beartype.door.TypeHinthas the exact same violation:TypeHint(Union[int, bool]) == TypeHint(int)isTruewhilehash(TypeHint(Union[int, bool])) != hash(TypeHint(int)). RoutingSignature.__hash__throughTypeHintWrapper(...)instead of the raw hint wouldn't fix anything, since it reduces to the identicalhash(self._hint)call.Why I'm not attempting a fix here
A correct fix needs a canonical form to hash semantically-equivalent-but-structurally-different hints (like
Union[int, bool]andint) to the same value — that's a much bigger problem than a local tweak, and arguably belongs upstream inbeartype.door.TypeHint(or requiresplumto maintain its own hint-canonicalization, independent of beartype). Flagging this as its own issue rather than folding a speculative fix into an unrelated PR.Context
Surfaced during review of #296 (unrelated
Any-vs-concrete-type fix) — see this comment. That PR doesn't introduce or worsen this: it's reproducible on unmodifiedmasterwith stablebeartype, and for the specificAnycase #296 actually removes one instance of the same violation (Signature(Any) == Signature(int)wasTrueunderbeartype>=0.23before #296's fix, while their hashes still differed).