Repository: python/cpython
Pull request: GH-101578: Normalize the current exception
This is a necessary step for implementing PEP 669, as instrumentation needs exceptions to be normalized.

Also fixes #101578.

<!-- gh-issue-number: gh-101578 -->
* Issue: gh-101578
<!-- /gh-issue-number -->


Problem statement:
`PyErr_SetObject()` behavior is strange and not as documented.
Briefly:
`PyErr_SetObject(exc_type, exc_val)` does not create a new exception iff `isinstance(exc_val, BaseException)`, but uses `exc_val` instead.

Callers of `PyErr_SetObject()` need various workarounds to handle this.

The long version:

Internally CPython handles exceptions as a triple `(type, value, traceback)`, but the language treats exceptions as a single value.

This a legacy of the olden days before proper exceptions.
To handle adding proper exceptions to Python, various error handling functions, specifically `_PyErr_SetObject` still treat exceptions as triples, with the convention that if the value is an exception, then the exception is already normalized.

One other oddity is that if `exc_val` is a tuple, it is treated as the `*` arguments to `exc_type` when calling it. So, if `isinstance(exc_val, BaseException)` the desired behavior can be achieved by wrapping `exc_val` in a one-tuple.

As a consequence, both `_PyErr_SetKeyError` and `_PyGen_SetStopIterationValue` are a lot more complex than they should be to workaround this behavior.

We could make `PyErr_SetObject` act as documented, but that is likely to break C extensions, given how old this behavior is, and that it is relied on throughout CPython.

Code that does the following is common:
```C
    exc = new_foo_exception();
    PyErr_SetObject(&PyFooException_Type, exc);
```

We could just document the current behavior, but the current behavior is strange.
What I suggest is this:
* Create a new API function, PyErr_SetException(exc)` that takes a single exception object.
* Document `PyErr_SetObject()` accurately
* Deprecate the old function


This is an old bug going back to the 2 series.

<!-- gh-linked-prs -->
### Linked PRs
* gh-101607
* gh-101962
* gh-102057
* gh-102157
* gh-102702
* gh-113369
* gh-113606
<!-- /gh-linked-prs -->

Also relevant:
* gh-102502

Hints from issue discussion:
The only case I can find of strange behavior leaking out is this:
```Py
import sys

def wrap_in_sys_exit(ex):
    try:
        try:
            raise ex(-1)
        except ex as err:
            sys.exit(err)
    except BaseException as err:
        return err

print(repr(wrap_in_sys_exit(ValueError)))
print(repr(wrap_in_sys_exit(SystemExit)))
```
Which prints
```
SystemExit(ValueError(-1))
SystemExit(-1)
```
instead of the expected
```
SystemExit(ValueError(-1))
SystemExit(SystemExit(-1))
This API goes by to 1997.  It is remarkable how simple and clear the code used to be:

```c
void
PyErr_Restore(PyObject *type, PyObject *value, PyObject *traceback)
{
	PyThreadState *tstate = PyThreadState_GET();
	PyObject *oldtype, *oldvalue, *oldtraceback;

	if (traceback != NULL && !PyTraceBack_Check(traceback)) {
		/* XXX Should never happen -- fatal error instead? */
		Py_DECREF(traceback);
		traceback = NULL;
	}

	/* Save these in locals to safeguard against recursive
	   invocation through Py_XDECREF */
	oldtype = tstate->curexc_type;
	oldvalue = tstate->curexc_value;
	oldtraceback = tstate->curexc_traceback;

	tstate->curexc_type = type;
	tstate->curexc_value = value;
	tstate->curexc_traceback = traceback;

	Py_XDECREF(oldtype);
	Py_XDECREF(oldvalue);
	Py_XDECREF(oldtraceback);
}

void
PyErr_SetObject(PyObject *exception, PyObject *value)
{
	Py_XINCREF(exception);
	Py_XINCREF(value);
	PyErr_Restore(exception, value, (PyObject *)NULL);
}
```

The code may have been simple, but it can leave the VM in a precarious state if the value is not a legal argument for calling the type.

Commit message: Fix typos

Patch to review:
diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst
index 087e0a61d12d59..402e2c83977bba 100644
--- a/Doc/c-api/exceptions.rst
+++ b/Doc/c-api/exceptions.rst
@@ -402,6 +402,8 @@ Querying the error indicator
 
 .. c:function:: void PyErr_Fetch(PyObject **ptype, PyObject **pvalue, PyObject **ptraceback)
 
+    As of 3.12, this function is deprecated. Use :c:func:`PyErr_Fetch1` instead.
+
    Retrieve the error indicator into three variables whose addresses are passed.
    If the error indicator is not set, set all three variables to ``NULL``.  If it is
    set, it will be cleared and you own a reference to each object retrieved.  The
@@ -422,6 +424,49 @@ Querying the error indicator
          }
 
 
+.. c:function:: PyObject *PyErr_Fetch1(void)
+
+    Returns the current error indicator, clearing the error indicator at the same time.
+
+   .. note::
+
+      This function is normally only used by code that needs to catch exceptions or
+      by code that needs to save and restore the error indicator temporarily, e.g.::
+
+         {
+            PyObject *exc = PyErr_Fetch1();
+
+            /* ... code that might produce other errors ... */
+
+            PyErr_Restore1(exc);
+         }
+
+   .. versionadded:: 3.12
+
+
+.. c:function:: void PyErr_Restore1(PyObject *exc)
+
+    As of 3.12, this function is deprecated. Use :c:func:`PyErr_Restore1` instead.
+
+   Set the error indicator to ``exc``.
+   If the error indicator is already set, it is cleared first.
+
+   ``exc`` should be a valid exception.
+   (Violating this rules will cause subtle problems later.)
+   This call consumes a reference to the ``exc`` object: you must own a
+   reference to that object before the call and after the call you no longer own
+   that reference.
+   (If you don't understand this, don't use this function. I warned you.)
+
+   .. note::
+
+      This function is normally only used by code that needs to save and restore the
+      error indicator temporarily.  Use :c:func:`PyErr_Fetch` to save the current
+      error indicator.
+
+   .. versionadded:: 3.12
+
+
 .. c:function:: void PyErr_Restore(PyObject *type, PyObject *value, PyObject *traceback)
 
    Set  the error indicator from the three objects.  If the error indicator is
@@ -437,12 +482,15 @@ Querying the error indicator
    .. note::
 
       This function is normally only used by code that needs to save and restore the
-      error indicator temporarily.  Use :c:func:`PyErr_Fetch` to save the current
+      error indicator temporarily.  Use :c:func:`PyErr_Fetch1` to save the current
       error indicator.
 
 
 .. c:function:: void PyErr_NormalizeException(PyObject **exc, PyObject **val, PyObject **tb)
 
+    As of 3.12, this function is deprecated.
+    Use :c:func:`PyErr_Fetch1` instead of :c:func:`PyErr_Fetch`.
+
    Under certain circumstances, the values returned by :c:func:`PyErr_Fetch` below
    can be "unnormalized", meaning that ``*exc`` is a class object but ``*val`` is
    not an instance of the  same class.  This function can be used to instantiate
@@ -704,6 +752,18 @@ Exception Objects
    :attr:`__suppress_context__` is implicitly set to ``True`` by this function.
 
 
+.. c:function:: PyObject* PyException_GetArgs(PyObject *ex)
+
+   Return args of the given exception as a new reference,
+   as accessible from Python through :attr:`args`.
+
+
+.. c:function:: void PyException_SetArgs(PyObject *ex, PyObject *args)
+
+   Set the args of the given exception,
+   as accessible from Python through :attr:`args`.
+
+
 .. _unicodeexceptions:
 
 Unicode Exception Objects
diff --git a/Include/cpython/pyerrors.h b/Include/cpython/pyerrors.h
index 141341667795e8..0d9cc9922f7368 100644
--- a/Include/cpython/pyerrors.h
+++ b/Include/cpython/pyerrors.h
@@ -99,6 +99,7 @@ PyAPI_FUNC(void) _PyErr_GetExcInfo(PyThreadState *, PyObject **, PyObject **, Py
 /* Context manipulation (PEP 3134) */
 
 PyAPI_FUNC(void) _PyErr_ChainExceptions(PyObject *, PyObject *, PyObject *);
+PyAPI_FUNC(void) _PyErr_ChainExceptions1(PyObject *);
 
 /* Like PyErr_Format(), but saves current exception as __context__ and
    __cause__.
diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h
index f5db52f76e8f4f..be1fcb61fa2244 100644
--- a/Include/cpython/pystate.h
+++ b/Include/cpython/pystate.h
@@ -166,9 +166,7 @@ struct _ts {
     PyObject *c_traceobj;
 
     /* The exception currently being raised */
-    PyObject *curexc_type;
-    PyObject *curexc_value;
-    PyObject *curexc_traceback;
+    PyObject *current_exception;
 
     /* Pointer to the top of the exception stack for the exceptions
      * we may be currently handling.  (See _PyErr_StackItem above.)
diff --git a/Include/internal/pycore_pyerrors.h b/Include/internal/pycore_pyerrors.h
index 66f37942ef916a..ec4e4631ae504f 100644
--- a/Include/internal/pycore_pyerrors.h
+++ b/Include/internal/pycore_pyerrors.h
@@ -20,7 +20,10 @@ extern void _PyErr_FiniTypes(PyInterpreterState *);
 static inline PyObject* _PyErr_Occurred(PyThreadState *tstate)
 {
     assert(tstate != NULL);
-    return tstate->curexc_type;
+    if (tstate->current_exception == NULL) {
+        return NULL;
+    }
+    return (PyObject *)Py_TYPE(tstate->current_exception);
 }
 
 static inline void _PyErr_ClearExcState(_PyErr_StackItem *exc_state)
@@ -37,10 +40,16 @@ PyAPI_FUNC(void) _PyErr_Fetch(
     PyObject **value,
     PyObject **traceback);
 
+extern PyObject *
+_PyErr_Fetch1(PyThreadState *tstate);
+
 PyAPI_FUNC(int) _PyErr_ExceptionMatches(
     PyThreadState *tstate,
     PyObject *exc);
 
+void
+_PyErr_Restore1(PyThreadState *tstate, PyObject *exc);
+
 PyAPI_FUNC(void) _PyErr_Restore(
     PyThreadState *tstate,
     PyObject *type,
diff --git a/Include/pyerrors.h b/Include/pyerrors.h
index d5ac6af5b32c6c..026bb7d5ffef18 100644
--- a/Include/pyerrors.h
+++ b/Include/pyerrors.h
@@ -18,6 +18,8 @@ PyAPI_FUNC(PyObject *) PyErr_Occurred(void);
 PyAPI_FUNC(void) PyErr_Clear(void);
 PyAPI_FUNC(void) PyErr_Fetch(PyObject **, PyObject **, PyObject **);
 PyAPI_FUNC(void) PyErr_Restore(PyObject *, PyObject *, PyObject *);
+PyAPI_FUNC(PyObject *) PyErr_Fetch1(void);
+PyAPI_FUNC(void) PyErr_Restore1(PyObject *);
 #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030b0000
 PyAPI_FUNC(PyObject*) PyErr_GetHandledException(void);
 PyAPI_FUNC(void) PyErr_SetHandledException(PyObject *);
@@ -51,6 +53,10 @@ PyAPI_FUNC(void) PyException_SetCause(PyObject *, PyObject *);
 PyAPI_FUNC(PyObject *) PyException_GetContext(PyObject *);
 PyAPI_FUNC(void) PyException_SetContext(PyObject *, PyObject *);
 
+
+PyAPI_FUNC(PyObject *) PyException_GetArgs(PyObject *);
+PyAPI_FUNC(void) PyException_SetArgs(PyObject *, PyObject *);
+
 /* */
 
 #define PyExceptionClass_Check(x)                                       \
diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py
index f629321458d8ae..4ae71e431c56dc 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -347,6 +347,7 @@ def test_capi2():
                 _testcapi.raise_exception(BadException, 0)
             except RuntimeError as err:
                 exc, err, tb = sys.exc_info()
+                tb = tb.tb_next
                 co = tb.tb_frame.f_code
                 self.assertEqual(co.co_name, "__init__")
                 self.assertTrue(co.co_filename.endswith('test_exceptions.py'))
@@ -1415,8 +1416,8 @@ def gen():
     @cpython_only
     def test_recursion_normalizing_infinite_exception(self):
         # Issue #30697. Test that a RecursionError is raised when
-        # PyErr_NormalizeException() maximum recursion depth has been
-        # exceeded.
+        # maximum recursion depth has been exceeded when creating
+        # an exception
         code = """if 1:
             import _testcapi
             try:
@@ -1426,8 +1427,7 @@ def test_recursion_normalizing_infinite_exception(self):
         """
         rc, out, err = script_helper.assert_python_failure("-c", code)
         self.assertEqual(rc, 1)
-        self.assertIn(b'RecursionError: maximum recursion depth exceeded '
-                      b'while normalizing an exception', err)
+        self.assertIn(b'RecursionError: maximum recursion depth exceeded', err)
         self.assertIn(b'Done.', out)
 
 
diff --git a/Misc/NEWS.d/next/C API/2023-02-06-16-14-30.gh-issue-101578.PW5fA9.rst b/Misc/NEWS.d/next/C API/2023-02-06-16-14-30.gh-issue-101578.PW5fA9.rst
new file mode 100644
index 00000000000000..3f731fa7c3cb79
--- /dev/null
+++ b/Misc/NEWS.d/next/C API/2023-02-06-16-14-30.gh-issue-101578.PW5fA9.rst	
@@ -0,0 +1,5 @@
+Deprecate the three arguments forms of :cfunc:`PyErr_Fetch` and
+:cfunc:`PyErr_Restore` and replace them with one argument forms that take
+and return the exception object. Also add :cfunc:`PyException_GetArgs` and
+:cfunc:`PyException_SetArgs` as convenience functions to help dealing with
+exceptions in the C API.
diff --git a/Modules/_testcapi/heaptype.c b/Modules/_testcapi/heaptype.c
index bf80fd64d80b35..ba836088256e1d 100644
--- a/Modules/_testcapi/heaptype.c
+++ b/Modules/_testcapi/heaptype.c
@@ -116,10 +116,9 @@ test_from_spec_invalid_metatype_inheritance(PyObject *self, PyObject *Py_UNUSED(
     PyObject *bases = NULL;
     PyObject *new = NULL;
     PyObject *meta_error_string = NULL;
-    PyObject *exc_type = NULL;
-    PyObject *exc_value = NULL;
-    PyObject *exc_traceback = NULL;
+    PyObject *exc = NULL;
     PyObject *result = NULL;
+    PyObject *message = NULL;
 
     metaclass_a = PyType_FromSpecWithBases(&MinimalMetaclass_spec, (PyObject*)&PyType_Type);
     if (metaclass_a == NULL) {
@@ -156,13 +155,19 @@ test_from_spec_invalid_metatype_inheritance(PyObject *self, PyObject *Py_UNUSED(
 
     // Assert that the correct exception was raised
     if (PyErr_ExceptionMatches(PyExc_TypeError)) {
-        PyErr_Fetch(&exc_type, &exc_value, &exc_traceback);
-
+        exc = PyErr_Fetch1();
+        PyObject *args = PyException_GetArgs(exc);
+        if (!PyTuple_Check(args) || PyTuple_Size(args) != 1) {
+            PyErr_SetString(PyExc_AssertionError,
+                    "TypeError args are not a one-tuple");
+            goto finally;
+        }
+        PyObject *message = Py_NewRef(PyTuple_GET_ITEM(args, 0));
         meta_error_string = PyUnicode_FromString("metaclass conflict:");
         if (meta_error_string == NULL) {
             goto finally;
         }
-        int res = PyUnicode_Contains(exc_value, meta_error_string);
+        int res = PyUnicode_Contains(message, meta_error_string);
         if (res < 0) {
             goto finally;
         }
@@ -179,9 +184,8 @@ test_from_spec_invalid_metatype_inheritance(PyObject *self, PyObject *Py_UNUSED(
     Py_XDECREF(bases);
     Py_XDECREF(new);
     Py_XDECREF(meta_error_string);
-    Py_XDECREF(exc_type);
-    Py_XDECREF(exc_value);
-    Py_XDECREF(exc_traceback);
+    Py_XDECREF(exc);
+    Py_XDECREF(message);
     Py_XDECREF(class_a);
     Py_XDECREF(class_b);
     return result;
diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c
index 6630faa6f4471d..7f129ec6c78a7b 100644
--- a/Modules/gcmodule.c
+++ b/Modules/gcmodule.c
@@ -2082,11 +2082,10 @@ PyGC_Collect(void)
         n = 0;
     }
     else {
-        PyObject *exc, *value, *tb;
         gcstate->collecting = 1;
-        _PyErr_Fetch(tstate, &exc, &value, &tb);
+        PyObject *exc = _PyErr_Fetch1(tstate);
         n = gc_collect_with_callback(tstate, NUM_GENERATIONS - 1);
-        _PyErr_Restore(tstate, exc, value, tb);
+        _PyErr_Restore1(tstate, exc);
         gcstate->collecting = 0;
     }
 
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index b9067213820b52..7b7f4380c99cdc 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -1663,15 +1663,15 @@ PyDict_GetItem(PyObject *op, PyObject *key)
 #endif
 
     /* Preserve the existing exception */
-    PyObject *exc_type, *exc_value, *exc_tb;
     PyObject *value;
     Py_ssize_t ix; (void)ix;
 
-    _PyErr_Fetch(tstate, &exc_type, &exc_value, &exc_tb);
+
+    PyObject *exc = _PyErr_Fetch1(tstate);
     ix = _Py_dict_lookup(mp, key, hash, &value);
 
     /* Ignore any exception raised by the lookup */
-    _PyErr_Restore(tstate, exc_type, exc_value, exc_tb);
+    _PyErr_Restore1(tstate, exc);
 
 
     assert(ix >= 0 || value == NULL);
diff --git a/Objects/exceptions.c b/Objects/exceptions.c
index db6f7d52804d6a..13b173f8f94026 100644
--- a/Objects/exceptions.c
+++ b/Objects/exceptions.c
@@ -8,6 +8,7 @@
 #include <Python.h>
 #include <stdbool.h>
 #include "pycore_ceval.h"         // _Py_EnterRecursiveCall
+#include "pycore_pyerrors.h"      // struct _PyErr_Restore1
 #include "pycore_exceptions.h"    // struct _Py_exc_state
 #include "pycore_initconfig.h"
 #include "pycore_object.h"
@@ -288,13 +289,17 @@ BaseException_set_tb(PyBaseExceptionObject *self, PyObject *tb, void *Py_UNUSED(
         PyErr_SetString(PyExc_TypeError, "__traceback__ may not be deleted");
         return -1;
     }
-    else if (!(tb == Py_None || PyTraceBack_Check(tb))) {
+    if (PyTraceBack_Check(tb)) {
+        Py_XSETREF(self->traceback, Py_NewRef(tb));
+    }
+    else if (tb == Py_None) {
+        Py_CLEAR(self->traceback);
+    }
+    else {
         PyErr_SetString(PyExc_TypeError,
                         "__traceback__ must be a traceback or None");
         return -1;
     }
-
-    Py_XSETREF(self->traceback, Py_NewRef(tb));
     return 0;
 }
 
@@ -413,6 +418,20 @@ PyException_SetContext(PyObject *self, PyObject *context)
     Py_XSETREF(_PyBaseExceptionObject_cast(self)->context, context);
 }
 
+PyObject *
+PyException_GetArgs(PyObject *self)
+{
+    PyObject *args = _PyBaseExceptionObject_cast(self)->args;
+    return Py_NewRef(args);
+}
+
+void
+PyException_SetArgs(PyObject *self, PyObject *args)
+{
+    Py_INCREF(args);
+    Py_XSETREF(_PyBaseExceptionObject_cast(self)->args, args);
+}
+
 const char *
 PyExceptionClass_Name(PyObject *ob)
 {
@@ -3188,20 +3207,19 @@ SimpleExtendsException(PyExc_Exception, ReferenceError,
 
 #define MEMERRORS_SAVE 16
 
+static PyBaseExceptionObject last_resort_memory_error;
+
 static PyObject *
-MemoryError_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+get_memory_error(int allow_allocation, PyObject *args, PyObject *kwds)
 {
     PyBaseExceptionObject *self;
-
-    /* If this is a subclass of MemoryError, don't use the freelist
-     * and just return a fresh object */
-    if (type != (PyTypeObject *) PyExc_MemoryError) {
-        return BaseException_new(type, args, kwds);
-    }
-
     struct _Py_exc_state *state = get_exc_state();
     if (state->memerrors_freelist == NULL) {
-        return BaseException_new(type, args, kwds);
+        if (!allow_allocation) {
+            return Py_NewRef(&last_resort_memory_error);
+        }
+        PyObject *result = BaseException_new((PyTypeObject *)PyExc_MemoryError, args, kwds);
+        return result;
     }
 
     /* Fetch object from freelist and revive it */
@@ -3221,6 +3239,35 @@ MemoryError_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
     return (PyObject *)self;
 }
 
+static PyBaseExceptionObject last_resort_memory_error;
+
+static PyObject *
+MemoryError_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+    /* If this is a subclass of MemoryError, don't use the freelist
+     * and just return a fresh object */
+    if (type != (PyTypeObject *) PyExc_MemoryError) {
+        return BaseException_new(type, args, kwds);
+    }
+    return get_memory_error(1, args, kwds);
+}
+
+PyObject *
+_PyErr_NoMemory(PyThreadState *tstate)
+{
+    if (Py_IS_TYPE(PyExc_MemoryError, NULL)) {
+        /* PyErr_NoMemory() has been called before PyExc_MemoryError has been
+           initialized by _PyExc_Init() */
+        Py_FatalError("Out of memory and PyExc_MemoryError is not "
+                      "initialized yet");
+    }
+    PyObject *err = get_memory_error(0, NULL, NULL);
+    if (err != NULL) {
+        _PyErr_Restore1(tstate, err);
+    }
+    return NULL;
+}
+
 static void
 MemoryError_dealloc(PyBaseExceptionObject *self)
 {
@@ -3252,6 +3299,7 @@ preallocate_memerrors(void)
     /* We create enough MemoryErrors and then decref them, which will fill
        up the freelist. */
     int i;
+
     PyObject *errors[MEMERRORS_SAVE];
     for (i = 0; i < MEMERRORS_SAVE; i++) {
         errors[i] = MemoryError_new((PyTypeObject *) PyExc_MemoryError,
@@ -3291,6 +3339,9 @@ static PyTypeObject _PyExc_MemoryError = {
 };
 PyObject *PyExc_MemoryError = (PyObject *) &_PyExc_MemoryError;
 
+static PyBaseExceptionObject last_resort_memory_error = {
+    _PyObject_IMMORTAL_INIT(&_PyExc_MemoryError)
+};
 
 /*
  *    BufferError extends Exception
diff --git a/Objects/object.c b/Objects/object.c
index e940222c657e3c..7817c04ef5f5be 100644
--- a/Objects/object.c
+++ b/Objects/object.c
@@ -2416,10 +2416,10 @@ _Py_Dealloc(PyObject *op)
     destructor dealloc = type->tp_dealloc;
 #ifdef Py_DEBUG
     PyThreadState *tstate = _PyThreadState_GET();
-    PyObject *old_exc_type = tstate != NULL ? tstate->curexc_type : NULL;
+    PyObject *old_exc = tstate != NULL ? tstate->current_exception : NULL;
     // Keep the old exception type alive to prevent undefined behavior
     // on (tstate->curexc_type != old_exc_type) below
-    Py_XINCREF(old_exc_type);
+    Py_XINCREF(old_exc);
     // Make sure that type->tp_name remains valid
     Py_INCREF(type);
 #endif
@@ -2432,12 +2432,12 @@ _Py_Dealloc(PyObject *op)
 #ifdef Py_DEBUG
     // gh-89373: The tp_dealloc function must leave the current exception
     // unchanged.
-    if (tstate != NULL && tstate->curexc_type != old_exc_type) {
+    if (tstate != NULL && tstate->current_exception != old_exc) {
         const char *err;
-        if (old_exc_type == NULL) {
+        if (old_exc == NULL) {
             err = "Deallocator of type '%s' raised an exception";
         }
-        else if (tstate->curexc_type == NULL) {
+        else if (tstate->current_exception == NULL) {
             err = "Deallocator of type '%s' cleared the current exception";
         }
         else {
@@ -2448,7 +2448,7 @@ _Py_Dealloc(PyObject *op)
         }
         _Py_FatalErrorFormat(__func__, err, type->tp_name);
     }
-    Py_XDECREF(old_exc_type);
+    Py_XDECREF(old_exc);
     Py_DECREF(type);
 #endif
 }
diff --git a/Parser/pegen.c b/Parser/pegen.c
index d84e06861edefc..52fce132008dd3 100644
--- a/Parser/pegen.c
+++ b/Parser/pegen.c
@@ -643,13 +643,10 @@ _PyPegen_number_token(Parser *p)
         PyThreadState *tstate = _PyThreadState_GET();
         // The only way a ValueError should happen in _this_ code is via
         // PyLong_FromString hitting a length limit.
-        if (tstate->curexc_type == PyExc_ValueError &&
-            tstate->curexc_value != NULL) {
-            PyObject *type, *value, *tb;
-            // This acts as PyErr_Clear() as we're replacing curexc.
-            PyErr_Fetch(&type, &value, &tb);
-            Py_XDECREF(tb);
-            Py_DECREF(type);
+        if (tstate->current_exception != NULL &&
+            Py_TYPE(tstate->current_exception) == (PyTypeObject *)PyExc_ValueError
+        ) {
+            PyObject *exc = PyErr_Fetch1();
             /* Intentionally omitting columns to avoid a wall of 1000s of '^'s
              * on the error message. Nobody is going to overlook their huge
              * numeric literal once given the line. */
@@ -659,8 +656,8 @@ _PyPegen_number_token(Parser *p)
                 t->end_lineno, -1 /* end_col_offset */,
                 "%S - Consider hexadecimal for huge integer literals "
                 "to avoid decimal conversion limits.",
-                value);
-            Py_DECREF(value);
+                exc);
+            Py_DECREF(exc);
         }
         return NULL;
     }
diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index 8993567ac82206..4b98320eca9c79 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -787,9 +787,7 @@ dummy_func(
                 DECREF_INPUTS();
             }
             else {
-                PyObject *exc_type = Py_NewRef(Py_TYPE(exc_value));
-                PyObject *exc_traceback = PyException_GetTraceback(exc_value);
-                _PyErr_Restore(tstate, exc_type, Py_NewRef(exc_value), exc_traceback);
+                _PyErr_Restore1(tstate, Py_NewRef(exc_value));
                 goto exception_unwind;
             }
         }
diff --git a/Python/ceval.c b/Python/ceval.c
index 2e6fed580dede4..e882fff824480a 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -2905,13 +2905,13 @@ format_kwargs_error(PyThreadState *tstate, PyObject *func, PyObject *kwargs)
         }
     }
     else if (_PyErr_ExceptionMatches(tstate, PyExc_KeyError)) {
-        PyObject *exc, *val, *tb;
-        _PyErr_Fetch(tstate, &exc, &val, &tb);
-        if (val && PyTuple_Check(val) && PyTuple_GET_SIZE(val) == 1) {
+        PyObject *exc = _PyErr_Fetch1(tstate);
+        PyObject *args = ((PyBaseExceptionObject *)exc)->args;
+        if (exc && PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1) {
             _PyErr_Clear(tstate);
             PyObject *funcstr = _PyObject_FunctionStr(func);
             if (funcstr != NULL) {
-                PyObject *key = PyTuple_GET_ITEM(val, 0);
+                PyObject *key = PyTuple_GET_ITEM(args, 0);
                 _PyErr_Format(
                     tstate, PyExc_TypeError,
                     "%U got multiple values for keyword argument '%S'",
@@ -2919,11 +2919,9 @@ format_kwargs_error(PyThreadState *tstate, PyObject *func, PyObject *kwargs)
                 Py_DECREF(funcstr);
             }
             Py_XDECREF(exc);
-            Py_XDECREF(val);
-            Py_XDECREF(tb);
         }
         else {
-            _PyErr_Restore(tstate, exc, val, tb);
+            _PyErr_Restore1(tstate, exc);
         }
     }
 }
diff --git a/Python/errors.c b/Python/errors.c
index 05ef62246ec0a4..8c2c635665001e 100644
--- a/Python/errors.c
+++ b/Python/errors.c
@@ -27,32 +27,43 @@ static PyObject *
 _PyErr_FormatV(PyThreadState *tstate, PyObject *exception,
                const char *format, va_list vargs);
 
+void
+_PyErr_Restore1(PyThreadState *tstate, PyObject *exc)
+{
+    PyObject *old_exc = tstate->current_exception;
+    tstate->current_exception = exc;
+    Py_XDECREF(old_exc);
+}
 
 void
 _PyErr_Restore(PyThreadState *tstate, PyObject *type, PyObject *value,
                PyObject *traceback)
 {
-    PyObject *oldtype, *oldvalue, *oldtraceback;
-
-    if (traceback != NULL && !PyTraceBack_Check(traceback)) {
-        /* XXX Should never happen -- fatal error instead? */
-        /* Well, it could be None. */
-        Py_SETREF(traceback, NULL);
+#ifdef Py_DEBUG
+    /* Check that we are being passed a normalized exception.
+     *
+     * Exceptions are normalized if all NULL,
+     * or if curexc_type = Py_TYPE(curexc_value) and
+     * curexc_traceback = curexc_value->traceback
+     * and both type and traceback are valid */
+    if (value == NULL) {
+        assert(type == NULL);
+        assert(traceback == NULL);
     }
+    else {
+        assert(PyExceptionClass_Check(type));
+        assert(type == (PyObject *)Py_TYPE(value));
+        assert(((PyBaseExceptionObject *)value)->traceback != Py_None);
+        assert(traceback == ((PyBaseExceptionObject *)value)->traceback ||
+            (traceback == Py_None &&
+            ((PyBaseExceptionObject *)value)->traceback == NULL)
+        );
+    }
+#endif
 
-    /* Save these in locals to safeguard against recursive
-       invocation through Py_XDECREF */
-    oldtype = tstate->curexc_type;
-    oldvalue = tstate->curexc_value;
-    oldtraceback = tstate->curexc_traceback;
-
-    tstate->curexc_type = type;
-    tstate->curexc_value = value;
-    tstate->curexc_traceback = traceback;
-
-    Py_XDECREF(oldtype);
-    Py_XDECREF(oldvalue);
-    Py_XDECREF(oldtraceback);
+    _PyErr_Restore1(tstate, value);
+    Py_XDECREF(type);
+    Py_XDECREF(traceback);
 }
 
 void
@@ -117,30 +128,29 @@ _PyErr_SetObject(PyThreadState *tstate, PyObject *exception, PyObject *value)
                       exception);
         return;
     }
-
     Py_XINCREF(value);
-    exc_value = _PyErr_GetTopmostException(tstate)->exc_value;
-    if (exc_value != NULL && exc_value != Py_None) {
-        /* Implicit exception chaining */
-        Py_INCREF(exc_value);
-        if (value == NULL || !PyExceptionInstance_Check(value)) {
-            /* We must normalize the value right now */
-            PyObject *fixed_value;
+    /* Normalize the exception */
+    if (value == NULL || (PyObject *)Py_TYPE(value) != exception) {
+        /* We must normalize the value right now */
+        PyObject *fixed_value;
 
-            /* Issue #23571: functions must not be called with an
-               exception set */
-            _PyErr_Clear(tstate);
-
-            fixed_value = _PyErr_CreateException(exception, value);
-            Py_XDECREF(value);
-            if (fixed_value == NULL) {
-                Py_DECREF(exc_value);
-                return;
-            }
+        /* Issue #23571: functions must not be called with an
+            exception set */
+        _PyErr_Clear(tstate);
 
-            value = fixed_value;
+        fixed_value = _PyErr_CreateException(exception, value);
+        Py_XDECREF(value);
+        if (fixed_value == NULL) {
+            return;
         }
 
+        value = fixed_value;
+    }
+
+    exc_value = _PyErr_GetTopmostException(tstate)->exc_value;
+    if (exc_value != NULL && exc_value != Py_None) {
+        /* Implicit exception chaining */
+        Py_INCREF(exc_value);
         /* Avoid creating new reference cycles through the
            context chain, while taking care not to hang on
            pre-existing ones.
@@ -414,17 +424,34 @@ PyErr_NormalizeException(PyObject **exc, PyObject **val, PyObject **tb)
 }
 
 
+PyObject *
+_PyErr_Fetch1(PyThreadState *tstate) {
+    PyObject *exc = tstate->current_exception;
+    tstate->current_exception = NULL;
+    return exc;
+}
+
+PyObject *
+PyErr_Fetch1(void)
+{
+    PyThreadState *tstate = _PyThreadState_GET();
+    return _PyErr_Fetch1(tstate);
+}
+
 void
 _PyErr_Fetch(PyThreadState *tstate, PyObject **p_type, PyObject **p_value,
              PyObject **p_traceback)
 {
-    *p_type = tstate->curexc_type;
-    *p_value = tstate->curexc_value;
-    *p_traceback = tstate->curexc_traceback;
-
-    tstate->curexc_type = NULL;
-    tstate->curexc_value = NULL;
-    tstate->curexc_traceback = NULL;
+    PyObject *exc = _PyErr_Fetch1(tstate);
+    *p_value = exc;
+    if (exc == NULL) {
+        *p_type = NULL;
+        *p_traceback = NULL;
+    }
+    else {
+        *p_type = Py_NewRef(Py_TYPE(exc));
+        *p_traceback = Py_XNewRef(((PyBaseExceptionObject *)exc)->traceback);
+    }
 }
 
 
@@ -597,6 +624,28 @@ _PyErr_ChainExceptions(PyObject *typ, PyObject *val, PyObject *tb)
     }
 }
 
+/* Like PyErr_Restore1(), but if an exception is already set,
+   set the context associated with it.
+
+   The caller is responsible for ensuring that this call won't create
+   any cycles in the exception context chain. */
+void
+_PyErr_ChainExceptions1(PyObject *exc)
+{
+    if (exc == NULL) {
+        return;
+    }
+    PyThreadState *tstate = _PyThreadState_GET();
+    if (_PyErr_Occurred(tstate)) {
+        PyObject *exc2 = _PyErr_Fetch1(tstate);
+        PyException_SetContext(exc2, exc);
+        _PyErr_Restore1(tstate, exc2);
+    }
+    else {
+        _PyErr_Restore1(tstate, exc);
+    }
+}
+
 /* Set the currently set exception's context to the given exception.
 
    If the provided exc_info is NULL, then the current Python thread state's
@@ -706,19 +755,6 @@ PyErr_BadArgument(void)
     return 0;
 }
 
-PyObject *
-_PyErr_NoMemory(PyThreadState *tstate)
-{
-    if (Py_IS_TYPE(PyExc_MemoryError, NULL)) {
-        /* PyErr_NoMemory() has been called before PyExc_MemoryError has been
-           initialized by _PyExc_Init() */
-        Py_FatalError("Out of memory and PyExc_MemoryError is not "
-                      "initialized yet");
-    }
-    _PyErr_SetNone(tstate, PyExc_MemoryError);
-    return NULL;
-}
-
 PyObject *
 PyErr_NoMemory(void)
 {
diff --git a/Python/import.c b/Python/import.c
index da6c15c5fd4144..1318c09d9b3212 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -1592,6 +1592,13 @@ remove_importlib_frames(PyThreadState *tstate)
         Py_DECREF(code);
         tb = next;
     }
+    assert(PyExceptionInstance_Check(value));
+    assert((PyObject *)Py_TYPE(value) == exception);
+    if (base_tb == NULL) {
+        base_tb = Py_None;
+        Py_INCREF(Py_None);
+    }
+    PyException_SetTraceback(value, base_tb);
 done:
     _PyErr_Restore(tstate, exception, value, base_tb);
 }
diff --git a/Python/initconfig.c b/Python/initconfig.c
index d7b2dc4a297425..9843de207553ba 100644
--- a/Python/initconfig.c
+++ b/Python/initconfig.c
@@ -3143,8 +3143,7 @@ init_dump_ascii_wstr(const wchar_t *str)
 void
 _Py_DumpPathConfig(PyThreadState *tstate)
 {
-    PyObject *exc_type, *exc_value, *exc_tb;
-    _PyErr_Fetch(tstate, &exc_type, &exc_value, &exc_tb);
+    PyObject *exc = _PyErr_Fetch1(tstate);
 
     PySys_WriteStderr("Python path configuration:\n");
 
@@ -3202,5 +3201,5 @@ _Py_DumpPathConfig(PyThreadState *tstate)
         PySys_WriteStderr("  ]\n");
     }
 
-    _PyErr_Restore(tstate, exc_type, exc_value, exc_tb);
+    _PyErr_Restore1(tstate, exc);
 }
diff --git a/Python/pystate.c b/Python/pystate.c
index 8bb49d954a81b6..401d67e303dd5c 100644
--- a/Python/pystate.c
+++ b/Python/pystate.c
@@ -1379,9 +1379,7 @@ PyThreadState_Clear(PyThreadState *tstate)
     Py_CLEAR(tstate->dict);
     Py_CLEAR(tstate->async_exc);
 
-    Py_CLEAR(tstate->curexc_type);
-    Py_CLEAR(tstate->curexc_value);
-    Py_CLEAR(tstate->curexc_traceback);
+    Py_CLEAR(tstate->current_exception);
 
     Py_CLEAR(tstate->exc_state.exc_value);
 
diff --git a/Python/pythonrun.c b/Python/pythonrun.c
index 35292b6478a833..6a4d593768690a 100644
--- a/Python/pythonrun.c
+++ b/Python/pythonrun.c
@@ -748,13 +748,10 @@ _Py_HandleSystemExit(int *exitcode_p)
     }
 
  done:
-    /* Restore and clear the exception info, in order to properly decref
-     * the exception, value, and traceback.      If we just exit instead,
-     * these leak, which confuses PYTHONDUMPREFS output, and may prevent
-     * some finalizers from running.
-     */
-    PyErr_Restore(exception, value, tb);
-    PyErr_Clear();
+    /* Cleanup the exception */
+    Py_CLEAR(exception);
+    Py_CLEAR(value);
+    Py_CLEAR(tb);
     *exitcode_p = exitcode;
     return 1;
 }
diff --git a/Python/sysmodule.c b/Python/sysmodule.c
index f9f766a94d1464..762188a13a2f28 100644
--- a/Python/sysmodule.c
+++ b/Python/sysmodule.c
@@ -66,12 +66,11 @@ _PySys_GetAttr(PyThreadState *tstate, PyObject *name)
     if (sd == NULL) {
         return NULL;
     }
-    PyObject *exc_type, *exc_value, *exc_tb;
-    _PyErr_Fetch(tstate, &exc_type, &exc_value, &exc_tb);
+    PyObject *exc = _PyErr_Fetch1(tstate);
     /* XXX Suppress a new exception if it was raised and restore
      * the old one. */
     PyObject *value = _PyDict_GetItemWithError(sd, name);
-    _PyErr_Restore(tstate, exc_type, exc_value, exc_tb);
+    _PyErr_Restore1(tstate, exc);
     return value;
 }
 
@@ -3704,11 +3703,10 @@ static void
 sys_format(PyObject *key, FILE *fp, const char *format, va_list va)
 {
     PyObject *file, *message;
-    PyObject *error_type, *error_value, *error_traceback;
     const char *utf8;
     PyThreadState *tstate = _PyThreadState_GET();
 
-    _PyErr_Fetch(tstate, &error_type, &error_value, &error_traceback);
+    PyObject *error = _PyErr_Fetch1(tstate);
     file = _PySys_GetAttr(tstate, key);
     message = PyUnicode_FromFormatV(format, va);
     if (message != NULL) {
@@ -3720,7 +3718,7 @@ sys_format(PyObject *key, FILE *fp, const char *format, va_list va)
         }
         Py_DECREF(message);
     }
-    _PyErr_Restore(tstate, error_type, error_value, error_traceback);
+    _PyErr_Restore1(tstate, error);
 }
 
 void
diff --git a/Python/traceback.c b/Python/traceback.c
index da26c9b260a3bd..6482a8bd9d7e35 100644
--- a/Python/traceback.c
+++ b/Python/traceback.c
@@ -249,6 +249,8 @@ PyTraceBack_Here(PyFrameObject *frame)
         _PyErr_ChainExceptions(exc, val, tb);
         return -1;
     }
+    assert(PyExceptionInstance_Check(val));
+    PyException_SetTraceback(val, newtb);
     PyErr_Restore(exc, val, newtb);
     Py_XDECREF(tb);
     return 0;
@@ -260,13 +262,12 @@ void _PyTraceback_Add(const char *funcname, const char *filename, int lineno)
     PyObject *globals;
     PyCodeObject *code;
     PyFrameObject *frame;
-    PyObject *exc, *val, *tb;
     PyThreadState *tstate = _PyThreadState_GET();
 
     /* Save and clear the current exception. Python functions must not be
        called with an exception set. Calling Python functions happens when
        the codec of the filesystem encoding is implemented in pure Python. */
-    _PyErr_Fetch(tstate, &exc, &val, &tb);
+    PyObject *exc = _PyErr_Fetch1(tstate);
 
     globals = PyDict_New();
     if (!globals)
@@ -283,13 +284,13 @@ void _PyTraceback_Add(const char *funcname, const char *filename, int lineno)
         goto error;
     frame->f_lineno = lineno;
 
-    _PyErr_Restore(tstate, exc, val, tb);
+    _PyErr_Restore1(tstate, exc);
     PyTraceBack_Here(frame);
     Py_DECREF(frame);
     return;
 
 error:
-    _PyErr_ChainExceptions(exc, val, tb);
+    _PyErr_ChainExceptions1(exc);
 }
 
 static PyObject *


Code review comments:
diff_hunk: @@ -422,6 +424,49 @@ Querying the error indicator
          }
 
 
+.. c:function:: PyObject *PyErr_Fetch1(void)
path: Doc/c-api/exceptions.rst
text: @user1:
I'd call them something like PyErr_GetRaisedException/PyErr_SetRaisedException.  We already have Get/SetHandledException.

diff_hunk: @@ -422,6 +424,49 @@ Querying the error indicator
          }
 
 
+.. c:function:: PyObject *PyErr_Fetch1(void)
+
+    Returns the current error indicator, clearing the error indicator at the same time.
+
+   .. note::
+
+      This function is normally only used by code that needs to catch exceptions or
+      by code that needs to save and restore the error indicator temporarily, e.g.::
+
+         {
+            PyObject *exc = PyErr_Fetch1();
+
+            /* ... code that might produce other errors ... */
+
+            PyErr_Restore1(exc);
+         }
+
+   .. versionadded:: 3.12
+
+
+.. c:function:: void PyErr_Restore1(PyObject *exc)
+
+    As of 3.12, this function is deprecated. Use :c:func:`PyErr_Restore1` instead.
path: Doc/c-api/exceptions.rst
text: @user1:
?? this is PyErr_Restore1

diff_hunk: @@ -422,6 +424,49 @@ Querying the error indicator
          }
 
 
+.. c:function:: PyObject *PyErr_Fetch1(void)
+
+    Returns the current error indicator, clearing the error indicator at the same time.
+
+   .. note::
+
+      This function is normally only used by code that needs to catch exceptions or
+      by code that needs to save and restore the error indicator temporarily, e.g.::
+
+         {
+            PyObject *exc = PyErr_Fetch1();
+
+            /* ... code that might produce other errors ... */
+
+            PyErr_Restore1(exc);
+         }
+
+   .. versionadded:: 3.12
+
+
+.. c:function:: void PyErr_Restore1(PyObject *exc)
+
+    As of 3.12, this function is deprecated. Use :c:func:`PyErr_Restore1` instead.
+
+   Set the error indicator to ``exc``.
path: Doc/c-api/exceptions.rst
text: @user1:
This looks like it's in the wrong place.