Skip to content

Make FrozenDict generic and fix its hashing - #17

Merged
ESultanik merged 1 commit into
masterfrom
utils-generics
Aug 7, 2026
Merged

Make FrozenDict generic and fix its hashing#17
ESultanik merged 1 commit into
masterfrom
utils-generics

Conversation

@ESultanik

Copy link
Copy Markdown
Owner

Stacked on #16.

FrozenDict was untyped, so index_type_map[4] gave Any and nothing about the mapping's key or value types survived to callers. It is now FrozenDict[K, V_co].

Written with TypeVar + Generic rather than PEP 695 class FrozenDict[K, V], since that syntax requires Python 3.12 and this package supports 3.10. The two are equivalent to a type checker; only the spelling and variance inference differ. V_co is covariant because the mapping is read-only.

Hashing

__hash__ had two problems. The isinstance(self._mapping, Hashable) branch was unreachable — dict sets __hash__ = None, so a dict is never Hashable — and the branch that actually ran hashed only self._mapping.keys(). Since Mapping.__eq__ compares values, two FrozenDicts with the same keys and different values were unequal but always collided:

hash(FrozenDict({'a': 1})) == hash(FrozenDict({'a': 2}))   # was True

Now hashes the items, falling back to the keys when a value is unhashable, which preserves the equal-implies-equal-hash requirement either way.

__init__ builds through an explicitly Any-typed local: keyword arguments can only contribute str keys, so inferring directly widened the key type to K | str and contradicted the declaration.

Verification

Tests go from one omnibus case to 13, covering mapping behaviour, immutability, hash/eq consistency, value-sensitive hashing, unhashable values, use as a dict key, and runtime parameterisation. ty is clean on the module.

🤖 Generated with Claude Code

https://claude.ai/code/session_01367hFob9sd4xpDmVT4uoFy

`FrozenDict` was untyped, so `index_type_map[4]` gave `Any` and nothing about the
mapping's key or value types survived to callers. It is now
`FrozenDict[K, V_co]`, written with `TypeVar` + `Generic` rather than PEP 695
`class FrozenDict[K, V]`, since that syntax needs Python 3.12 and this package
supports 3.10. The two are equivalent to a type checker; only the spelling and
variance inference differ. `V_co` is covariant because the mapping is read-only.

`__hash__` had two problems. The `isinstance(self._mapping, Hashable)` branch was
unreachable -- `dict` sets `__hash__ = None`, so a dict is never Hashable -- and
the branch that actually ran hashed only `self._mapping.keys()`. Since
`Mapping.__eq__` compares values, two FrozenDicts with the same keys and
different values were unequal but always collided:

    hash(FrozenDict({'a': 1})) == hash(FrozenDict({'a': 2}))   # was True

Now hashes the items, falling back to the keys when a value is unhashable, which
preserves the equal-implies-equal-hash requirement either way.

`__init__` builds through an explicitly `Any`-typed local: keyword arguments can
only contribute `str` keys, so inferring directly widened the key type to
`K | str` and contradicted the declaration. `ty` is clean on the module.

Tests go from one omnibus case to 13, covering mapping behaviour, immutability,
hash/eq consistency, value-sensitive hashing, unhashable values, use as a dict
key, and runtime parameterisation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01367hFob9sd4xpDmVT4uoFy
Base automatically changed from iowrapper-fixes to master August 7, 2026 19:31
@ESultanik
ESultanik merged commit 9f6dc5b into master Aug 7, 2026
11 checks passed
@ESultanik
ESultanik deleted the utils-generics branch August 7, 2026 19:32
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.

1 participant