Skip to content

Fix LangStr hash/eq contract violation with plain str (closes #255) - #261

Open
trungminhdo4-glitch wants to merge 1 commit into
opensanctions:mainfrom
trungminhdo4-glitch:fix/issue-255-langstr-hash-eq
Open

Fix LangStr hash/eq contract violation with plain str (closes #255)#261
trungminhdo4-glitch wants to merge 1 commit into
opensanctions:mainfrom
trungminhdo4-glitch:fix/issue-255-langstr-hash-eq

Conversation

@trungminhdo4-glitch

Copy link
Copy Markdown

Problem

LangStr.__hash__ returned hash((text, lang)) but __eq__ falls back to
value-only comparison with plain str. This violates Python's
a == b => hash(a) == hash(b) invariant: a LangStr that compares equal to a
plain str has a different hash, making it unfindable in str-keyed dicts/sets.

Closes #255.

Reproduction

from rigour.langs.text import LangStr
a = LangStr("foo", "eng")
a == "foo"                      # True
hash(a) == hash("foo")          # False  -- broken
{"foo": 1}.get(a)               # None   -- not found
{"foo", a}                      # 2 elements instead of 1

Root Cause

In rigour/langs/text.py, __hash__ always encodes the (content, lang)
tuple, but __eq__ ignores lang when the other operand is a plain str.

Change

     def __hash__(self) -> int:
-        return hash((super().__str__(), self.lang))
+        return hash(str(self))

This follows Option 1 from the issue: hash like a plain str, preserving the
documented "transparent string" behavior. Two LangStrs that differ only in
lang share a hash bucket (acceptable - equality still separates them).

Tests

Updated the existing test assertion and added 5 regression tests:

  • test_langstr_hash_eq_contract: a == b => hash(a) == hash(b)
  • test_langstr_str_keyed_dict: lookup in str-keyed dict works
  • test_langstr_str_keyed_set: set deduplication works
  • test_langstr_lang_differing: same text, different lang share hash
  • test_langstr_none_lang: None lang satisfies contract

All 6 lang tests pass.

Compatibility

  • No public API changes
  • __hash__ still returns int
  • __eq__ unchanged
  • Existing behavior preserved for LangStr == LangStr (lang-sensitive)

Risks

Very low. The fix aligns __hash__ with the documented promise that LangStr
"behaves like a regular string." Hash collisions between LangStrs with different
languages are benign.

Out of Scope

  • Option 2 (making equality lang-sensitive across the board)
  • Any changes to other modules

Change __hash__ to return hash(str(self)) so that LangStr instances
equal to a plain str also share the same hash. This restores the
Python data model invariant a == b => hash(a) == hash(b).

Previously __hash__ returned hash((text, lang)) which always included
the language code, while __eq__ fell back to value-only comparison
when compared with a plain str (no .lang attribute). This meant
LangStr instances could not be found in str-keyed dicts or sets.

Two LangStrs with the same text but different languages now share
a hash bucket, which is fine because __eq__ still separates them.
@trungminhdo4-glitch
trungminhdo4-glitch marked this pull request as ready for review July 25, 2026 23:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LangStr violates the hash/eq contract with plain str

1 participant