]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-132825: Enhance unhashable error messages for dict and set (#132828)
authorVictor Stinner <vstinner@python.org>
Wed, 23 Apr 2025 15:10:09 +0000 (17:10 +0200)
committerGitHub <noreply@github.com>
Wed, 23 Apr 2025 15:10:09 +0000 (17:10 +0200)
Lib/test/test_capi/test_abstract.py
Lib/test/test_dict.py
Lib/test/test_import/__init__.py
Lib/test/test_set.py
Misc/NEWS.d/next/Core_and_Builtins/2025-04-23-11-34-39.gh-issue-132825._yv0uL.rst [new file with mode: 0644]
Objects/dictobject.c
Objects/setobject.c

index 912c2de2b6993094f80d0292d059bf505c65d2be..7d548ae87c0fa6fa1bac13c470f65ec694eadea8 100644 (file)
@@ -460,7 +460,8 @@ class CAPITest(unittest.TestCase):
             self.assertFalse(haskey({}, []))
             self.assertEqual(cm.unraisable.exc_type, TypeError)
             self.assertEqual(str(cm.unraisable.exc_value),
-                             "unhashable type: 'list'")
+                             "cannot use 'list' as a dict key "
+                             "(unhashable type: 'list')")
 
         with support.catch_unraisable_exception() as cm:
             self.assertFalse(haskey([], 1))
index 7756c1f995cf2c5fed2a1ba75571073072c7bb87..9485ef2889f7600dd13c85f457e353e86a48710d 100644 (file)
@@ -3,6 +3,7 @@ import collections.abc
 import gc
 import pickle
 import random
+import re
 import string
 import sys
 import unittest
@@ -1487,6 +1488,47 @@ class DictTest(unittest.TestCase):
                 self.assertEqual(d.get(key3_3), 44)
                 self.assertGreaterEqual(eq_count, 1)
 
+    def test_unhashable_key(self):
+        d = {'a': 1}
+        key = [1, 2, 3]
+
+        def check_unhashable_key():
+            msg = "cannot use 'list' as a dict key (unhashable type: 'list')"
+            return self.assertRaisesRegex(TypeError, re.escape(msg))
+
+        with check_unhashable_key():
+            key in d
+        with check_unhashable_key():
+            d[key]
+        with check_unhashable_key():
+            d[key] = 2
+        with check_unhashable_key():
+            d.setdefault(key, 2)
+        with check_unhashable_key():
+            d.pop(key)
+        with check_unhashable_key():
+            d.get(key)
+
+        # Only TypeError exception is overriden,
+        # other exceptions are left unchanged.
+        class HashError:
+            def __hash__(self):
+                raise KeyError('error')
+
+        key2 = HashError()
+        with self.assertRaises(KeyError):
+            key2 in d
+        with self.assertRaises(KeyError):
+            d[key2]
+        with self.assertRaises(KeyError):
+            d[key2] = 2
+        with self.assertRaises(KeyError):
+            d.setdefault(key2, 2)
+        with self.assertRaises(KeyError):
+            d.pop(key2)
+        with self.assertRaises(KeyError):
+            d.get(key2)
+
 
 class CAPITest(unittest.TestCase):
 
index a745760289b5b8e896c24ba3a202e3f21ccd0d1b..b5f4645847a1e64444b6e42e166a9c60e086edb9 100644 (file)
@@ -1055,7 +1055,7 @@ except TypeError as e:
 """)
             popen = script_helper.spawn_python("main.py", cwd=tmp)
             stdout, stderr = popen.communicate()
-            self.assertEqual(stdout.rstrip(), b"unhashable type: 'substr'")
+            self.assertIn(b"unhashable type: 'substr'", stdout.rstrip())
 
             with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
                 f.write("""
@@ -1072,7 +1072,7 @@ except TypeError as e:
 
             popen = script_helper.spawn_python("main.py", cwd=tmp)
             stdout, stderr = popen.communicate()
-            self.assertEqual(stdout.rstrip(), b"unhashable type: 'substr'")
+            self.assertIn(b"unhashable type: 'substr'", stdout.rstrip())
 
             # Various issues with sys module
             with open(os.path.join(tmp, "main.py"), "w", encoding='utf-8') as f:
index a8531d466e56e735fb523477db3963c3f33c8fd6..c01e323553d768bef7f1b7631da0d0db0df7f63c 100644 (file)
@@ -1,16 +1,17 @@
-import unittest
-from test import support
-from test.support import warnings_helper
+import collections.abc
+import copy
 import gc
-import weakref
+import itertools
 import operator
-import copy
 import pickle
-from random import randrange, shuffle
+import re
+import unittest
 import warnings
-import collections
-import collections.abc
-import itertools
+import weakref
+from random import randrange, shuffle
+from test import support
+from test.support import warnings_helper
+
 
 class PassThru(Exception):
     pass
@@ -645,6 +646,35 @@ class TestSet(TestJointOps, unittest.TestCase):
         self.assertRaises(KeyError, myset.remove, set(range(1)))
         self.assertRaises(KeyError, myset.remove, set(range(3)))
 
+    def test_unhashable_element(self):
+        myset = {'a'}
+        elem = [1, 2, 3]
+
+        def check_unhashable_element():
+            msg = "cannot use 'list' as a set element (unhashable type: 'list')"
+            return self.assertRaisesRegex(TypeError, re.escape(msg))
+
+        with check_unhashable_element():
+            elem in myset
+        with check_unhashable_element():
+            myset.add(elem)
+        with check_unhashable_element():
+            myset.discard(elem)
+
+        # Only TypeError exception is overriden,
+        # other exceptions are left unchanged.
+        class HashError:
+            def __hash__(self):
+                raise KeyError('error')
+
+        elem2 = HashError()
+        with self.assertRaises(KeyError):
+            elem2 in myset
+        with self.assertRaises(KeyError):
+            myset.add(elem2)
+        with self.assertRaises(KeyError):
+            myset.discard(elem2)
+
 
 class SetSubclass(set):
     pass
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-23-11-34-39.gh-issue-132825._yv0uL.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-23-11-34-39.gh-issue-132825._yv0uL.rst
new file mode 100644 (file)
index 0000000..d751837
--- /dev/null
@@ -0,0 +1,2 @@
+Enhance unhashable key/element error messages for :class:`dict` and
+:class:`set`. Patch by Victor Stinner.
index ff6dbb8bed3007abd96c888b549000f0532cb100..c34d17b2259be3641e79023db074f965df7e1e5e 100644 (file)
@@ -2276,6 +2276,22 @@ PyDict_GetItem(PyObject *op, PyObject *key)
             "PyDict_GetItemRef() or PyDict_GetItemWithError()");
 }
 
+static void
+dict_unhashtable_type(PyObject *key)
+{
+    PyObject *exc = PyErr_GetRaisedException();
+    assert(exc != NULL);
+    if (!Py_IS_TYPE(exc, (PyTypeObject*)PyExc_TypeError)) {
+        PyErr_SetRaisedException(exc);
+        return;
+    }
+
+    PyErr_Format(PyExc_TypeError,
+                 "cannot use '%T' as a dict key (%S)",
+                 key, exc);
+    Py_DECREF(exc);
+}
+
 Py_ssize_t
 _PyDict_LookupIndex(PyDictObject *mp, PyObject *key)
 {
@@ -2286,6 +2302,7 @@ _PyDict_LookupIndex(PyDictObject *mp, PyObject *key)
 
     Py_hash_t hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        dict_unhashtable_type(key);
         return -1;
     }
 
@@ -2382,6 +2399,7 @@ PyDict_GetItemRef(PyObject *op, PyObject *key, PyObject **result)
 
     Py_hash_t hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        dict_unhashtable_type(key);
         *result = NULL;
         return -1;
     }
@@ -2397,6 +2415,7 @@ _PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject *key, PyObject **
 
     Py_hash_t hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        dict_unhashtable_type(key);
         *result = NULL;
         return -1;
     }
@@ -2434,6 +2453,7 @@ PyDict_GetItemWithError(PyObject *op, PyObject *key)
     }
     hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        dict_unhashtable_type(key);
         return NULL;
     }
 
@@ -2591,6 +2611,7 @@ setitem_take2_lock_held(PyDictObject *mp, PyObject *key, PyObject *value)
     assert(PyDict_Check(mp));
     Py_hash_t hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        dict_unhashtable_type(key);
         Py_DECREF(key);
         Py_DECREF(value);
         return -1;
@@ -2742,6 +2763,7 @@ PyDict_DelItem(PyObject *op, PyObject *key)
     assert(key);
     Py_hash_t hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        dict_unhashtable_type(key);
         return -1;
     }
 
@@ -3064,6 +3086,7 @@ pop_lock_held(PyObject *op, PyObject *key, PyObject **result)
 
     Py_hash_t hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        dict_unhashtable_type(key);
         if (result) {
             *result = NULL;
         }
@@ -3398,6 +3421,7 @@ dict_subscript(PyObject *self, PyObject *key)
 
     hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        dict_unhashtable_type(key);
         return NULL;
     }
     ix = _Py_dict_lookup_threadsafe(mp, key, hash, &value);
@@ -4278,6 +4302,7 @@ dict_get_impl(PyDictObject *self, PyObject *key, PyObject *default_value)
 
     hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        dict_unhashtable_type(key);
         return NULL;
     }
     ix = _Py_dict_lookup_threadsafe(self, key, hash, &val);
@@ -4310,6 +4335,7 @@ dict_setdefault_ref_lock_held(PyObject *d, PyObject *key, PyObject *default_valu
 
     hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        dict_unhashtable_type(key);
         if (result) {
             *result = NULL;
         }
@@ -4737,8 +4763,8 @@ int
 PyDict_Contains(PyObject *op, PyObject *key)
 {
     Py_hash_t hash = _PyObject_HashFast(key);
-
     if (hash == -1) {
+        dict_unhashtable_type(key);
         return -1;
     }
 
@@ -6829,6 +6855,7 @@ _PyDict_SetItem_LockHeld(PyDictObject *dict, PyObject *name, PyObject *value)
     if (value == NULL) {
         Py_hash_t hash = _PyObject_HashFast(name);
         if (hash == -1) {
+            dict_unhashtable_type(name);
             return -1;
         }
         return delitem_knownhash_lock_held((PyObject *)dict, name, hash);
index 347888389b8dcd72a9b2f5552c0c24920b9891fa..73cebbe7e1ecdfb2f10c9494ed5a691c6312bcb4 100644 (file)
@@ -211,11 +211,28 @@ set_add_entry(PySetObject *so, PyObject *key, Py_hash_t hash)
     return set_add_entry_takeref(so, Py_NewRef(key), hash);
 }
 
+static void
+set_unhashtable_type(PyObject *key)
+{
+    PyObject *exc = PyErr_GetRaisedException();
+    assert(exc != NULL);
+    if (!Py_IS_TYPE(exc, (PyTypeObject*)PyExc_TypeError)) {
+        PyErr_SetRaisedException(exc);
+        return;
+    }
+
+    PyErr_Format(PyExc_TypeError,
+                 "cannot use '%T' as a set element (%S)",
+                 key, exc);
+    Py_DECREF(exc);
+}
+
 int
 _PySet_AddTakeRef(PySetObject *so, PyObject *key)
 {
     Py_hash_t hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        set_unhashtable_type(key);
         Py_DECREF(key);
         return -1;
     }
@@ -384,6 +401,7 @@ set_add_key(PySetObject *so, PyObject *key)
 {
     Py_hash_t hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        set_unhashtable_type(key);
         return -1;
     }
     return set_add_entry(so, key, hash);
@@ -394,6 +412,7 @@ set_contains_key(PySetObject *so, PyObject *key)
 {
     Py_hash_t hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        set_unhashtable_type(key);
         return -1;
     }
     return set_contains_entry(so, key, hash);
@@ -404,6 +423,7 @@ set_discard_key(PySetObject *so, PyObject *key)
 {
     Py_hash_t hash = _PyObject_HashFast(key);
     if (hash == -1) {
+        set_unhashtable_type(key);
         return -1;
     }
     return set_discard_entry(so, key, hash);