Thanks to visit codestin.com
Credit goes to github.com

Skip to content

gh-132825: Enhance unhashable error messages for dict and set #132828

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
gh-132825: Change unhashable key error messages for dict and set
  • Loading branch information
vstinner committed Apr 23, 2025
commit b23bd8db51a15327f98aec445e493ec59de7ce9c
3 changes: 2 additions & 1 deletion Lib/test/test_capi/test_abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
42 changes: 42 additions & 0 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import gc
import pickle
import random
import re
import string
import sys
import unittest
Expand Down Expand Up @@ -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):

Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("""
Expand All @@ -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:
Expand Down
48 changes: 39 additions & 9 deletions Lib/test/test_set.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Enhance unhashable key/element error messages for :class:`dict` and
:class:`set`. Patch by Victor Stinner.
29 changes: 28 additions & 1 deletion Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -2434,6 +2453,7 @@ PyDict_GetItemWithError(PyObject *op, PyObject *key)
}
hash = _PyObject_HashFast(key);
if (hash == -1) {
dict_unhashtable_type(key);
return NULL;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
Expand Down
20 changes: 20 additions & 0 deletions Objects/setobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
Loading