From cfe880a942045f352475a9cfee34fcc6c3084f37 Mon Sep 17 00:00:00 2001 From: Ivan Zorin Date: Fri, 6 Nov 2026 18:17:26 +0300 Subject: [PATCH 1/3] gh-161433: Add tuple item context to hashing errors --- Lib/test/test_tuple.py | 44 +++++++++++++++++++ ...-11-06-15-15-00.gh-issue-161433.A9b3Kq.rst | 2 + Objects/tupleobject.c | 18 ++++++++ 3 files changed, 64 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-15-15-00.gh-issue-161433.A9b3Kq.rst diff --git a/Lib/test/test_tuple.py b/Lib/test/test_tuple.py index 5650dec09adef43..6c28adc0524f283 100644 --- a/Lib/test/test_tuple.py +++ b/Lib/test/test_tuple.py @@ -18,6 +18,50 @@ class TupleTest(seq_tests.CommonTest): type2test = tuple + def test_hash_unhashable_item_note(self): + with self.assertRaisesRegex(TypeError, "unhashable type: 'list'") as cm: + hash(("alice", [])) + self.assertEqual(cm.exception.__notes__, + ["when hashing tuple item 1"]) + + def test_hash_nested_unhashable_item_notes(self): + with self.assertRaisesRegex(TypeError, "unhashable type: 'list'") as cm: + hash((("alice", []), 42)) + self.assertEqual(cm.exception.__notes__, + ["when hashing tuple item 1", + "when hashing tuple item 0"]) + + def test_hash_custom_type_error_preserved(self): + class BrokenHash: + def __hash__(self): + raise TypeError("custom failure") + + with self.assertRaisesRegex(TypeError, "custom failure") as cm: + hash((BrokenHash(),)) + self.assertEqual(cm.exception.__notes__, + ["when hashing tuple item 0"]) + + def test_hash_existing_exception_notes_preserved(self): + class BrokenHash: + def __hash__(self): + exc = TypeError("custom failure") + exc.add_note("from custom hash") + raise exc + + with self.assertRaisesRegex(TypeError, "custom failure") as cm: + hash((BrokenHash(),)) + self.assertEqual(cm.exception.__notes__, + ["from custom hash", "when hashing tuple item 0"]) + + def test_hash_non_type_error_preserved(self): + class BrokenHash: + def __hash__(self): + raise ValueError("custom failure") + + with self.assertRaisesRegex(ValueError, "custom failure") as cm: + hash((BrokenHash(),)) + self.assertFalse(hasattr(cm.exception, "__notes__")) + def test_getitem_error(self): t = () msg = "tuple indices must be integers or slices" diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-15-15-00.gh-issue-161433.A9b3Kq.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-15-15-00.gh-issue-161433.A9b3Kq.rst new file mode 100644 index 000000000000000..251837ac6d632a4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-06-15-15-00.gh-issue-161433.A9b3Kq.rst @@ -0,0 +1,2 @@ +Add context notes to :exc:`TypeError` raised while hashing a +:class:`tuple`, identifying the index of the item whose hash failed. diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 68ca2772507295e..992699756ac770f 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -8,6 +8,7 @@ #include "pycore_list.h" // _Py_memory_repeat() #include "pycore_modsupport.h" // _PyArg_NoKwnames() #include "pycore_object.h" // _PyObject_GC_TRACK() +#include "pycore_pyerrors.h" // _PyException_AddNote() #include "pycore_stackref.h" // PyStackRef_AsPyObjectSteal() #include "pycore_tuple.h" // _PyTupleIterObject @@ -392,6 +393,23 @@ tuple_hash(PyObject *op) for (Py_ssize_t i = 0; i < len; i++) { Py_uhash_t lane = PyObject_Hash(item[i]); if (lane == (Py_uhash_t)-1) { + if (PyErr_ExceptionMatches(PyExc_TypeError)) { + /* Preserve the original error while providing context for + the element that could not be hashed. */ + PyObject *exc = PyErr_GetRaisedException(); + PyObject *note = PyUnicode_FromFormat( + "when hashing tuple item %zd", i); + if (note != NULL) { + if (_PyException_AddNote(exc, note) < 0) { + PyErr_Clear(); + } + Py_DECREF(note); + } + else { + PyErr_Clear(); + } + PyErr_SetRaisedException(exc); + } _Py_LeaveRecursiveCall(); return -1; } From c7ce3914bbb7d3b7e4743fe917974cef286ee59a Mon Sep 17 00:00:00 2001 From: Ivan Zorin Date: Fri, 6 Nov 2026 22:55:31 +0300 Subject: [PATCH 2/3] gh-161433: Consolidate tuple hashing note tests --- Lib/test/test_tuple.py | 83 ++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 44 deletions(-) diff --git a/Lib/test/test_tuple.py b/Lib/test/test_tuple.py index 6c28adc0524f283..a3bd60eca4f6b15 100644 --- a/Lib/test/test_tuple.py +++ b/Lib/test/test_tuple.py @@ -18,50 +18,6 @@ class TupleTest(seq_tests.CommonTest): type2test = tuple - def test_hash_unhashable_item_note(self): - with self.assertRaisesRegex(TypeError, "unhashable type: 'list'") as cm: - hash(("alice", [])) - self.assertEqual(cm.exception.__notes__, - ["when hashing tuple item 1"]) - - def test_hash_nested_unhashable_item_notes(self): - with self.assertRaisesRegex(TypeError, "unhashable type: 'list'") as cm: - hash((("alice", []), 42)) - self.assertEqual(cm.exception.__notes__, - ["when hashing tuple item 1", - "when hashing tuple item 0"]) - - def test_hash_custom_type_error_preserved(self): - class BrokenHash: - def __hash__(self): - raise TypeError("custom failure") - - with self.assertRaisesRegex(TypeError, "custom failure") as cm: - hash((BrokenHash(),)) - self.assertEqual(cm.exception.__notes__, - ["when hashing tuple item 0"]) - - def test_hash_existing_exception_notes_preserved(self): - class BrokenHash: - def __hash__(self): - exc = TypeError("custom failure") - exc.add_note("from custom hash") - raise exc - - with self.assertRaisesRegex(TypeError, "custom failure") as cm: - hash((BrokenHash(),)) - self.assertEqual(cm.exception.__notes__, - ["from custom hash", "when hashing tuple item 0"]) - - def test_hash_non_type_error_preserved(self): - class BrokenHash: - def __hash__(self): - raise ValueError("custom failure") - - with self.assertRaisesRegex(ValueError, "custom failure") as cm: - hash((BrokenHash(),)) - self.assertFalse(hasattr(cm.exception, "__notes__")) - def test_getitem_error(self): t = () msg = "tuple indices must be integers or slices" @@ -494,6 +450,45 @@ def test_lexicographic_ordering(self): self.assertLess(a, b) self.assertLess(b, c) + def test_hash_unhashable_note(self): + with self.assertRaisesRegex(TypeError, "unhashable type: 'list'") as cm: + hash(("alice", [])) + self.assertEqual(cm.exception.__notes__, + ["when hashing tuple item 1"]) + + with self.assertRaisesRegex(TypeError, "unhashable type: 'list'") as cm: + hash((("alice", []), 42)) + self.assertEqual(cm.exception.__notes__, + ["when hashing tuple item 1", + "when hashing tuple item 0"]) + + class BrokenHash: + def __hash__(self): + raise TypeError("custom failure") + + with self.assertRaisesRegex(TypeError, "custom failure") as cm: + hash((BrokenHash(),)) + self.assertEqual(cm.exception.__notes__, ["when hashing tuple item 0"]) + + class BrokenHashNote: + def __hash__(self): + exc = TypeError("custom failure") + exc.add_note("from custom hash") + raise exc + + with self.assertRaisesRegex(TypeError, "custom failure") as cm: + hash((BrokenHashNote(),)) + self.assertEqual(cm.exception.__notes__, + ["from custom hash", "when hashing tuple item 0"]) + + class BrokenHashValueError: + def __hash__(self): + raise ValueError("custom failure") + + with self.assertRaisesRegex(ValueError, "custom failure") as cm: + hash((BrokenHashValueError(),)) + self.assertFalse(hasattr(cm.exception, "__notes__")) + # Notes on testing hash codes. The primary thing is that Python doesn't # care about "random" hash codes. To the contrary, we like them to be # very regular when possible, so that the low-order bits are as evenly From 0910da14a487a3a7907c2e874645ce687d65a847 Mon Sep 17 00:00:00 2001 From: Ivan Zorin Date: Sat, 7 Nov 2026 10:28:42 +0300 Subject: [PATCH 3/3] gh-161433: Acknowledge original issue author WakaTaira --- Misc/ACKS | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/ACKS b/Misc/ACKS index 65f6f423eb7c428..4178ae3108128aa 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -2048,6 +2048,7 @@ Sjoerd de Vries Jonas Wagner Daniel Wagner-Hall Niki W. Waibel +WakaTaira Wojtek Walczak Charles Waldman Richard Walker