diff --git a/Include/internal/pycore_pyerrors.h b/Include/internal/pycore_pyerrors.h
index 1023dbc3395b2f..e38472ab13a9df 100644
--- a/Include/internal/pycore_pyerrors.h
+++ b/Include/internal/pycore_pyerrors.h
@@ -29,7 +29,8 @@ PyAPI_FUNC(PyObject*) _PyErr_FormatFromCause(
...
);
-extern int _PyException_AddNote(
+// Export for 'pyexpat' shared extension.
+PyAPI_FUNC(int) _PyException_AddNote(
PyObject *exc,
PyObject *note);
diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
index cace780f79f515..aaa91aca36e3c4 100644
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -510,6 +510,34 @@ def _test_exception(self, have_source):
self.assertIn('call_with_frame("StartElement"',
entries[1].line)
+ def test_invalid_NotStandalone(self):
+ parser = expat.ParserCreate()
+ parser.NotStandaloneHandler = mock.Mock(return_value="bad value")
+ parser.ElementDeclHandler = lambda _1, _2: None
+
+ payload = b"""\
+]>
+"""
+ with self.assertRaises(TypeError) as cm:
+ parser.Parse(payload, True)
+ parser.NotStandaloneHandler.assert_called_once()
+
+ notes = ["invalid 'NotStandalone' event handler return value"]
+ self.assertEqual(cm.exception.__notes__, notes)
+
+ def test_invalid_ExternalEntityRefHandler(self):
+ parser = expat.ParserCreate()
+ parser.UseForeignDTD()
+ parser.SetParamEntityParsing(expat.XML_PARAM_ENTITY_PARSING_ALWAYS)
+ parser.ExternalEntityRefHandler = mock.Mock(return_value=None)
+
+ with self.assertRaises(TypeError) as cm:
+ parser.Parse(b"", True)
+ parser.ExternalEntityRefHandler.assert_called_once()
+
+ notes = ["invalid 'ExternalEntityRef' event handler return value"]
+ self.assertEqual(cm.exception.__notes__, notes)
+
# Test Current* members:
class PositionTest(unittest.TestCase):
diff --git a/Misc/NEWS.d/next/Library/2026-03-28-11-31-32.gh-issue-146563.cXtSym.rst b/Misc/NEWS.d/next/Library/2026-03-28-11-31-32.gh-issue-146563.cXtSym.rst
new file mode 100644
index 00000000000000..2103024b616d4e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-03-28-11-31-32.gh-issue-146563.cXtSym.rst
@@ -0,0 +1,2 @@
+:mod:`xml.parsers.expat`: add an exception note when a custom Expat handler
+return value cannot be properly interpreted. Patch by Bénédikt Tran.
diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c
index 31b883fe8bd548..0f0afe17513ef1 100644
--- a/Modules/pyexpat.c
+++ b/Modules/pyexpat.c
@@ -503,6 +503,28 @@ my_StartElementHandler(void *userData,
}
}
+static inline void
+invalid_expat_handler_rv(const char *name)
+{
+ PyObject *exc = PyErr_GetRaisedException();
+ assert(exc != NULL);
+ PyObject *note = PyUnicode_FromFormat("invalid '%s' event handler return value", name);
+ if (note == NULL) {
+ goto error;
+ }
+ int rc = _PyException_AddNote(exc, note);
+ Py_DECREF(note);
+ if (rc < 0) {
+ goto error;
+ };
+ goto done;
+
+error:
+ PyErr_Clear();
+done:
+ PyErr_SetRaisedException(exc);
+}
+
#define RC_HANDLER(RETURN_TYPE, NAME, PARAMS, \
INIT, PARSE_FORMAT, CONVERSION, \
RETURN_VARIABLE, GETUSERDATA) \
@@ -536,6 +558,9 @@ my_ ## NAME ## Handler PARAMS { \
} \
CONVERSION \
Py_DECREF(rv); \
+ if (PyErr_Occurred()) { \
+ invalid_expat_handler_rv(#NAME); \
+ } \
return RETURN_VARIABLE; \
}