Skip to content

Signature.__hash__ / __eq__ hash invariant violated for semantically-equivalent (but non-identical) types #297

Description

@nstarman

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions