diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index b2f4363b23e748..f85e9644ef0b93 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1630,6 +1630,26 @@ def __hash__(self): with self.assertRaises(KeyError): d.get(key2) + def test_unhashable_key_preserves_exception_info(self): + """gh-149313: exception notes and cause must survive the improved message.""" + class Key: + def __hash__(self): + try: + hash([]) + except TypeError as e: + e.add_note("custom note") + raise + + with self.assertRaises(TypeError) as cm: + {Key(): 1} + + exc = cm.exception + self.assertIn("cannot use", str(exc)) + # The original exception is chained as __cause__ + self.assertIsNotNone(exc.__cause__) + self.assertIn("unhashable type: 'list'", str(exc.__cause__)) + self.assertIn("custom note", exc.__cause__.__notes__[0]) + def test_clear_at_lookup(self): # gh-140551 dict crash if clear is called at lookup stage class X: diff --git a/Lib/test/test_set.py b/Lib/test/test_set.py index 9bfd4bc7d63669..0608aa9c8e8fed 100644 --- a/Lib/test/test_set.py +++ b/Lib/test/test_set.py @@ -686,6 +686,25 @@ def __hash__(self): with self.assertRaises(KeyError): myset.discard(elem2) + def test_unhashable_element_preserves_exception_info(self): + """gh-149313: exception notes and cause must survive the improved message.""" + class Elem: + def __hash__(self): + try: + hash([]) + except TypeError as e: + e.add_note("custom note") + raise + + with self.assertRaises(TypeError) as cm: + {Elem()} + + exc = cm.exception + self.assertIn("cannot use", str(exc)) + self.assertIsNotNone(exc.__cause__) + self.assertIn("unhashable type: 'list'", str(exc.__cause__)) + self.assertIn("custom note", exc.__cause__.__notes__[0]) + def test_hash_collision_remove_add(self): self.maxDiff = None # There should be enough space, so all elements with unique hash diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-07-23-09-44.gh-issue-149313.MnAD9T.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-07-23-09-44.gh-issue-149313.MnAD9T.rst new file mode 100644 index 00000000000000..324e515c84113d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-07-23-09-44.gh-issue-149313.MnAD9T.rst @@ -0,0 +1,3 @@ +Preserve original exception's ``__notes__``, ``__traceback__``, and +``__cause__`` when rewriting :exc:`TypeError` for unhashable dict keys and +set elements. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 42bc63acd9049c..9482abb3ef20f4 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -2416,7 +2416,11 @@ dict_unhashable_type(PyObject *op, PyObject *key) errmsg = "cannot use '%T' as a dict key (%S)"; } PyErr_Format(PyExc_TypeError, errmsg, key, exc); + PyObject *exc2 = PyErr_GetRaisedException(); + PyException_SetCause(exc2, Py_NewRef(exc)); + PyException_SetContext(exc2, Py_NewRef(exc)); Py_DECREF(exc); + PyErr_SetRaisedException(exc2); } Py_ssize_t diff --git a/Objects/setobject.c b/Objects/setobject.c index 1e630563604552..1c002efae0d65e 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -355,7 +355,11 @@ set_unhashable_type(PyObject *key) PyErr_Format(PyExc_TypeError, "cannot use '%T' as a set element (%S)", key, exc); + PyObject *exc2 = PyErr_GetRaisedException(); + PyException_SetCause(exc2, Py_NewRef(exc)); + PyException_SetContext(exc2, Py_NewRef(exc)); Py_DECREF(exc); + PyErr_SetRaisedException(exc2); } int