From b13add2d5728c966ec3e6c3168ebc3b35acec4f1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 17 Feb 2026 14:56:55 +0100 Subject: [PATCH 1/2] gh-141510: Optimize hash(frozendict) hash(frozendict) no longer creates a temporary items view and a temporary frozenset object. Copy frozenset_hash() code to frozendict_hash(). --- Lib/test/test_dict.py | 9 +++++++ Objects/dictobject.c | 56 ++++++++++++++++++++++++++++++------------- Objects/setobject.c | 5 +++- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 1db3559a012fd3..2a106a8a4e8739 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1775,6 +1775,15 @@ class MyFrozenDict(frozendict): d = MyFrozenDict(x=1, y=2) self.assertEqual(repr(d), "MyFrozenDict({'x': 1, 'y': 2})") + def test_hash(self): + # hash() doesn't rely on the items order + self.assertEqual(hash(frozendict(x=1, y=2)), + hash(frozendict(y=2, x=1))) + + fd = frozendict(x=[1], y=[2]) + with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"): + hash(fd) + if __name__ == "__main__": unittest.main() diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 46b0148cf59ab5..5104a50f3e3c7c 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7869,33 +7869,55 @@ frozendict_repr(PyObject *self) return res; } +static Py_uhash_t +_shuffle_bits(Py_uhash_t h) +{ + return ((h ^ 89869747UL) ^ (h << 16)) * 3644798167UL; +} + +// Code copied from frozenset_hash() static Py_hash_t frozendict_hash(PyObject *op) { PyFrozenDictObject *self = _PyFrozenDictObject_CAST(op); - Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ma_hash); - if (hash != -1) { - return hash; + Py_hash_t shash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ma_hash); + if (shash != -1) { + return shash; } - PyObject *items = _PyDictView_New(op, &PyDictItems_Type); - if (items == NULL) { - return -1; - } - PyObject *frozenset = PyFrozenSet_New(items); - Py_DECREF(items); - if (frozenset == NULL) { - return -1; + PyDictObject *mp = _PyAnyDict_CAST(op); + Py_uhash_t hash = 0; + + PyObject *key, *value; // borrowed refs + Py_ssize_t pos = 0; + while (PyDict_Next(op, &pos, &key, &value)) { + Py_hash_t key_hash = PyObject_Hash(key); + if (key_hash == -1) { + return -1; + } + hash ^= _shuffle_bits(key_hash); + + Py_hash_t value_hash = PyObject_Hash(value); + if (value_hash == -1) { + return -1; + } + hash ^= _shuffle_bits(value_hash); } - hash = PyObject_Hash(frozenset); - Py_DECREF(frozenset); - if (hash == -1) { - return -1; + /* Factor in the number of active entries */ + hash ^= ((Py_uhash_t)mp->ma_used + 1) * 1927868237UL; + + /* Disperse patterns arising in nested frozensets */ + hash ^= (hash >> 11) ^ (hash >> 25); + hash = hash * 69069U + 907133923UL; + + /* -1 is reserved as an error code */ + if (hash == (Py_uhash_t)-1) { + hash = 590923713UL; } - FT_ATOMIC_STORE_SSIZE_RELAXED(self->ma_hash, hash); - return hash; + FT_ATOMIC_STORE_SSIZE_RELAXED(self->ma_hash, (Py_hash_t)hash); + return (Py_hash_t)hash; } diff --git a/Objects/setobject.c b/Objects/setobject.c index 5d4d1812282eed..f8713bf3d1a432 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -964,7 +964,10 @@ _shuffle_bits(Py_uhash_t h) This hash algorithm can be used on either a frozenset or a set. When it is used on a set, it computes the hash value of the equivalent - frozenset without creating a new frozenset object. */ + frozenset without creating a new frozenset object. + + If you update this code, update also frozendict_hash() which copied this + code. */ static Py_hash_t frozenset_hash_impl(PyObject *self) From 390119d10e320eccc500ddcbc95a0caa3b800883 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 17 Feb 2026 15:39:10 +0100 Subject: [PATCH 2/2] Update comment --- Objects/dictobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 5104a50f3e3c7c..9cf6c9b4c8a9dd 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7907,7 +7907,7 @@ frozendict_hash(PyObject *op) /* Factor in the number of active entries */ hash ^= ((Py_uhash_t)mp->ma_used + 1) * 1927868237UL; - /* Disperse patterns arising in nested frozensets */ + /* Disperse patterns arising in nested frozendicts */ hash ^= (hash >> 11) ^ (hash >> 25); hash = hash * 69069U + 907133923UL;