From 733dd3318d1deaf35f17343859a84b1cc3d0cbb8 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Tue, 27 May 2025 20:29:10 +0000 Subject: [PATCH 1/3] gh-132617: Fix `dict.update() ` mutation check Use `ma_used` instead of `ma_keys->dk_nentries` for modification check so that we only check if the dictionary is modified, not if new keys are added to a different dictionary that shared the same keys object. --- Lib/test/test_dict.py | 32 +++++++++++++++++++ ...-05-27-20-29-00.gh-issue-132617.EmUfQQ.rst | 3 ++ Objects/dictobject.c | 4 +-- 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-05-27-20-29-00.gh-issue-132617.EmUfQQ.rst diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 52c38e42eca02d..c300d2dbcf831c 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -290,6 +290,38 @@ def badgen(): ['Cannot convert dictionary update sequence element #0 to a sequence'], ) + def test_update_shared_keys(self): + class MyClass: pass + + # Subclass str to enable us to create an object during the + # dict.update() call. + class MyStr(str): + def __hash__(self): + return super().__hash__() + + def __eq__(self, other): + # Create an object that shares the same PyDictKeysObject as + # the dict + obj2 = MyClass() + obj2.a = "a" + obj2.b = "b" + obj2.c = "c" + return super().__eq__(other) + + obj = MyClass() + obj.a = "a" + obj.b = "b" + + x = {} + x[MyStr("a")] = MyStr("a") + + # gh-132617: this previously raised "dict mutated during update" error + x.update(obj.__dict__) + + self.assertEqual(x, { + MyStr("a"): "a", + "b": "b", + }) def test_fromkeys(self): self.assertEqual(dict.fromkeys('abc'), {'a':None, 'b':None, 'c':None}) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-05-27-20-29-00.gh-issue-132617.EmUfQQ.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-27-20-29-00.gh-issue-132617.EmUfQQ.rst new file mode 100644 index 00000000000000..53aef541e64c23 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-05-27-20-29-00.gh-issue-132617.EmUfQQ.rst @@ -0,0 +1,3 @@ +Fix :meth:`dict.update` modification check that could incorrectly raise a +"dict mutated during update" error when a different dictionary was modified +that happens to share the same underlying keys object. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index fd8ccf56324207..72901377cbb3da 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -3858,7 +3858,7 @@ dict_dict_merge(PyInterpreterState *interp, PyDictObject *mp, PyDictObject *othe } } - Py_ssize_t orig_size = other->ma_keys->dk_nentries; + Py_ssize_t orig_size = other->ma_used; Py_ssize_t pos = 0; Py_hash_t hash; PyObject *key, *value; @@ -3892,7 +3892,7 @@ dict_dict_merge(PyInterpreterState *interp, PyDictObject *mp, PyDictObject *othe if (err != 0) return -1; - if (orig_size != other->ma_keys->dk_nentries) { + if (orig_size != other->ma_used) { PyErr_SetString(PyExc_RuntimeError, "dict mutated during update"); return -1; From a6ed4ecccdecbcad017f6dba1535ce80cb7cf82a Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 28 May 2025 09:26:30 -0400 Subject: [PATCH 2/3] Update comment --- Lib/test/test_dict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index c300d2dbcf831c..c6a932c652bf51 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -301,7 +301,7 @@ def __hash__(self): def __eq__(self, other): # Create an object that shares the same PyDictKeysObject as - # the dict + # the obj.__dict__. obj2 = MyClass() obj2.a = "a" obj2.b = "b" From 5867156837ac6a5431c3a36826d6eadbab84e109 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Wed, 28 May 2025 09:27:12 -0400 Subject: [PATCH 3/3] Minor edit --- Lib/test/test_dict.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index c6a932c652bf51..60c62430370e96 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -301,7 +301,7 @@ def __hash__(self): def __eq__(self, other): # Create an object that shares the same PyDictKeysObject as - # the obj.__dict__. + # obj.__dict__. obj2 = MyClass() obj2.a = "a" obj2.b = "b"