Make FrozenDict generic and fix its hashing - #17
Merged
Conversation
`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
ESultanik
force-pushed
the
iowrapper-fixes
branch
from
August 7, 2026 19:25
80759c9 to
6c96d30
Compare
ESultanik
force-pushed
the
utils-generics
branch
from
August 7, 2026 19:25
5154c48 to
60210ff
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #16.
FrozenDictwas untyped, soindex_type_map[4]gaveAnyand nothing about the mapping's key or value types survived to callers. It is nowFrozenDict[K, V_co].Written with
TypeVar+Genericrather than PEP 695class 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_cois covariant because the mapping is read-only.Hashing
__hash__had two problems. Theisinstance(self._mapping, Hashable)branch was unreachable —dictsets__hash__ = None, so a dict is neverHashable— and the branch that actually ran hashed onlyself._mapping.keys(). SinceMapping.__eq__compares values, twoFrozenDicts with the same keys and different values were unequal but always collided: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 explicitlyAny-typed local: keyword arguments can only contributestrkeys, so inferring directly widened the key type toK | strand 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.
tyis clean on the module.🤖 Generated with Claude Code
https://claude.ai/code/session_01367hFob9sd4xpDmVT4uoFy