https://github.com/python/cpython/commit/426449d9834855fcf8c150889157af8c39526b81
commit: 426449d9834855fcf8c150889157af8c39526b81
branch: main
author: Victor Stinner <[email protected]>
committer: cfbolz <[email protected]>
date: 2025-04-23T17:10:09+02:00
summary:
gh-132825: Enhance unhashable error messages for dict and set (#132828)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2025-04-23-11-34-39.gh-issue-132825._yv0uL.rst
M Lib/test/test_capi/test_abstract.py
M Lib/test/test_dict.py
M Lib/test/test_import/__init__.py
M Lib/test/test_set.py
M Objects/dictobject.c
M Objects/setobject.c
diff --git a/Lib/test/test_capi/test_abstract.py
b/Lib/test/test_capi/test_abstract.py
index 912c2de2b69930..7d548ae87c0fa6 100644
--- a/Lib/test/test_capi/test_abstract.py
+++ b/Lib/test/test_capi/test_abstract.py
@@ -460,7 +460,8 @@ def test_mapping_haskey(self):
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))
diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py
index 7756c1f995cf2c..9485ef2889f760 100644
--- a/Lib/test/test_dict.py
+++ b/Lib/test/test_dict.py
@@ -3,6 +3,7 @@
import gc
import pickle
import random
+import re
import string
import sys
import unittest
@@ -1487,6 +1488,47 @@ def make_pairs():
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):
diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py
index a745760289b5b8..b5f4645847a1e6 100644
--- a/Lib/test/test_import/__init__.py
+++ b/Lib/test/test_import/__init__.py
@@ -1055,7 +1055,7 @@ class substr(str):
""")
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 @@ class substr(str):
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:
diff --git a/Lib/test/test_set.py b/Lib/test/test_set.py
index a8531d466e56e7..c01e323553d768 100644
--- a/Lib/test/test_set.py
+++ b/Lib/test/test_set.py
@@ -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 @@ def test_set_membership(self):
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
index 00000000000000..d751837c44aac6
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-23-11-34-39.gh-issue-132825._yv0uL.rst
@@ -0,0 +1,2 @@
+Enhance unhashable key/element error messages for :class:`dict` and
+:class:`set`. Patch by Victor Stinner.
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index ff6dbb8bed3007..c34d17b2259be3 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -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);
diff --git a/Objects/setobject.c b/Objects/setobject.c
index 347888389b8dcd..73cebbe7e1ecdf 100644
--- a/Objects/setobject.c
+++ b/Objects/setobject.c
@@ -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);
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]