https://github.com/python/cpython/commit/646853df13492e2260befd5a13dba29af3c6be46
commit: 646853df13492e2260befd5a13dba29af3c6be46
branch: main
author: Alex Malyshev <[email protected]>
committer: vstinner <[email protected]>
date: 2026-05-06T15:01:12Z
summary:

gh-145559: Add PyUnstable_DumpTraceback() and PyUnstable_DumpTracebackThreads() 
(#148145)

Co-authored-by: Petr Viktorin <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>

files:
A Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst
M Doc/c-api/exceptions.rst
M Doc/whatsnew/3.15.rst
M Include/cpython/traceback.h
M Include/internal/pycore_traceback.h
M Modules/faulthandler.c
M Platforms/emscripten/node_entry.mjs
M Platforms/emscripten/web_example_pyrepl_jspi/src.mjs
M Python/pylifecycle.c
M Python/traceback.c
M configure
M configure.ac

diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst
index 7a07818b7b4d1a..2f8f108ee27f6a 100644
--- a/Doc/c-api/exceptions.rst
+++ b/Doc/c-api/exceptions.rst
@@ -1348,3 +1348,67 @@ Tracebacks
 
    This function returns ``0`` on success, and returns ``-1`` with an
    exception set on failure.
+
+.. c:function:: const char* PyUnstable_DumpTraceback(int fd, PyThreadState 
*tstate)
+
+   Write a trace of the Python stack in *tstate* into the file *fd*.  The 
format
+   looks like::
+
+      Traceback (most recent call first):
+        File "xxx", line xxx in <xxx>
+        File "xxx", line xxx in <xxx>
+        ...
+        File "xxx", line xxx in <xxx>
+
+   This function is meant to debug situations such as segfaults, fatal errors,
+   and similar. The file and function names it outputs are encoded to ASCII 
with
+   backslashreplace and truncated to 500 characters. It writes only the first
+   100 frames; further frames are truncated with the line ``...``.
+
+   This function will return ``NULL`` on success, or an error message on error.
+
+   This function is intended for use in crash scenarios such as signal handlers
+   for SIGSEGV, where the interpreter may be in an inconsistent state. Given
+   that it reads interpreter data structures that may be partially modified, 
the
+   function might produce incomplete output or it may even crash itself.
+
+   The caller does not need to hold an :term:`attached thread state`, nor does
+   *tstate* need to be attached.
+
+   .. versionadded:: next
+
+.. c:function:: const char* PyUnstable_DumpTracebackThreads(int fd, 
PyInterpreterState *interp, PyThreadState *current_tstate, Py_ssize_t 
max_threads)
+
+   Write the traces of all Python threads in *interp* into the file *fd*.
+
+   If *interp* is ``NULL`` then this function will try to identify the current
+   interpreter using thread-specific storage. If it cannot, it will return an
+   error.
+
+   If *current_tstate* is not ``NULL`` then it will be used to identify what 
the
+   current thread is in the written output. If it is ``NULL`` then this 
function
+   will identify the current thread using thread-specific storage. It is not an
+   error if the function is unable to get the current Python thread state.
+
+   This function will return ``NULL`` on success, or an error message on error.
+
+   This function is meant to debug debug situations such as segfaults, fatal
+   errors, and similar. It calls :c:func:`PyUnstable_DumpTraceback` for each
+   thread. It only writes the tracebacks of the first *max_threads* threads,
+   further output is truncated with the line ``...``. If *max_threads* is 0, 
the
+   function will use a default value of 100 for the argument.
+
+   This function is intended for use in crash scenarios such as signal handlers
+   for SIGSEGV, where the interpreter may be in an inconsistent state. Given
+   that it reads interpreter data structures that may be partially modified, 
the
+   function might produce incomplete output or it may even crash itself.
+
+   The caller does not need to hold an :term:`attached thread state`, nor does
+   *current_tstate* need to be attached.
+
+   .. warning::
+      On the :term:`free-threaded build`, this function is not thread-safe. If
+      another thread deletes its :term:`thread state` while this function is 
being
+      called, the process will likely crash.
+
+   .. versionadded:: next
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 2ca28378e6ef73..f3bcdd5f1a35b9 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -2352,6 +2352,11 @@ New features
   Python 3.14.
   (Contributed by Victor Stinner in :gh:`142417`.)
 
+* Add :c:func:`PyUnstable_DumpTraceback` and
+  :c:func:`PyUnstable_DumpTracebackThreads` functions to output Python
+  stacktraces.
+  (Contributed by Alex Malyshev in :gh:`145559`.)
+
 Changed C APIs
 --------------
 
diff --git a/Include/cpython/traceback.h b/Include/cpython/traceback.h
index 81c51944f136f2..7f42730f1b0919 100644
--- a/Include/cpython/traceback.h
+++ b/Include/cpython/traceback.h
@@ -11,3 +11,11 @@ struct _traceback {
     int tb_lasti;
     int tb_lineno;
 };
+
+PyAPI_FUNC(const char*) PyUnstable_DumpTraceback(int fd, PyThreadState 
*tstate);
+
+PyAPI_FUNC(const char*) PyUnstable_DumpTracebackThreads(
+    int fd,
+    PyInterpreterState *interp,
+    PyThreadState *current_tstate,
+    Py_ssize_t max_threads);
diff --git a/Include/internal/pycore_traceback.h 
b/Include/internal/pycore_traceback.h
index fbf6bc2c41f51d..e016afaa5c5687 100644
--- a/Include/internal/pycore_traceback.h
+++ b/Include/internal/pycore_traceback.h
@@ -14,56 +14,6 @@ PyAPI_FUNC(int) _Py_DisplaySourceLine(PyObject *, PyObject 
*, int, int, int *, P
 // Export for 'pyexact' shared extension
 PyAPI_FUNC(void) _PyTraceback_Add(const char *, const char *, int);
 
-/* Write the Python traceback into the file 'fd'. For example:
-
-       Traceback (most recent call first):
-         File "xxx", line xxx in <xxx>
-         File "xxx", line xxx in <xxx>
-         ...
-         File "xxx", line xxx in <xxx>
-
-   This function is written for debug purpose only, to dump the traceback in
-   the worst case: after a segmentation fault, at fatal error, etc. That's why,
-   it is very limited. Strings are truncated to 100 characters and encoded to
-   ASCII with backslashreplace. It doesn't write the source code, only the
-   function name, filename and line number of each frame. Write only the first
-   100 frames: if the traceback is truncated, write the line " ...".
-
-   This function is signal safe. */
-
-extern void _Py_DumpTraceback(
-    int fd,
-    PyThreadState *tstate);
-
-/* Write the traceback of all threads into the file 'fd'. current_thread can be
-   NULL.
-
-   Return NULL on success, or an error message on error.
-
-   This function is written for debug purpose only. It calls
-   _Py_DumpTraceback() for each thread, and so has the same limitations. It
-   only write the traceback of the first 100 threads: write "..." if there are
-   more threads.
-
-   If current_tstate is NULL, the function tries to get the Python thread state
-   of the current thread. It is not an error if the function is unable to get
-   the current Python thread state.
-
-   If interp is NULL, the function tries to get the interpreter state from
-   the current Python thread state, or from
-   _PyGILState_GetInterpreterStateUnsafe() in last resort.
-
-   It is better to pass NULL to interp and current_tstate, the function tries
-   different options to retrieve this information.
-
-   This function is signal safe. */
-
-extern const char* _Py_DumpTracebackThreads(
-    int fd,
-    PyInterpreterState *interp,
-    PyThreadState *current_tstate,
-    Py_ssize_t max_threads);
-
 /* Write a Unicode object into the file descriptor fd. Encode the string to
    ASCII using the backslashreplace error handler.
 
diff --git 
a/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst 
b/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst
new file mode 100644
index 00000000000000..9495d42160a9cd
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst
@@ -0,0 +1,3 @@
+Rename ``_Py_DumpTraceback`` and ``_Py_DumpTracebackThreads`` to
+:c:func:`PyUnstable_DumpTraceback` and
+:c:func:`PyUnstable_DumpTracebackThreads`.
diff --git a/Modules/faulthandler.c b/Modules/faulthandler.c
index 923f6f5b56d32b..1b4f0c2302daae 100644
--- a/Modules/faulthandler.c
+++ b/Modules/faulthandler.c
@@ -7,7 +7,7 @@
 #include "pycore_runtime.h"       // _Py_ID()
 #include "pycore_signal.h"        // Py_NSIG
 #include "pycore_time.h"          // _PyTime_FromSecondsObject()
-#include "pycore_traceback.h"     // _Py_DumpTracebackThreads
+#include "pycore_traceback.h"     // _Py_DumpStack()
 #ifdef HAVE_UNISTD_H
 #  include <unistd.h>             // _exit()
 #endif
@@ -206,14 +206,15 @@ faulthandler_dump_traceback(int fd, int all_threads,
     PyThreadState *tstate = PyGILState_GetThisThreadState();
 
     if (all_threads == 1) {
-        (void)_Py_DumpTracebackThreads(fd, NULL, tstate, max_threads);
+        (void)PyUnstable_DumpTracebackThreads(fd, NULL, tstate, max_threads);
     }
     else {
         if (all_threads == FT_IGNORE_ALL_THREADS) {
             PUTS(fd, "<Cannot show all threads while the GIL is disabled>\n");
         }
-        if (tstate != NULL)
-            _Py_DumpTraceback(fd, tstate);
+        if (tstate != NULL) {
+            PyUnstable_DumpTraceback(fd, tstate);
+        }
     }
 
     reentrant = 0;
@@ -277,17 +278,18 @@ faulthandler_dump_traceback_py_impl(PyObject *module, 
PyObject *file,
         /* gh-128400: Accessing other thread states while they're running
          * isn't safe if those threads are running. */
         _PyEval_StopTheWorld(interp);
-        errmsg = _Py_DumpTracebackThreads(fd, NULL, tstate, max_threads);
+        errmsg = PyUnstable_DumpTracebackThreads(fd, NULL, tstate, 
max_threads);
         _PyEval_StartTheWorld(interp);
-        if (errmsg != NULL) {
-            PyErr_SetString(PyExc_RuntimeError, errmsg);
-            Py_XDECREF(file);
-            return NULL;
-        }
     }
     else {
-        _Py_DumpTraceback(fd, tstate);
+        errmsg = PyUnstable_DumpTraceback(fd, tstate);
     }
+    if (errmsg != NULL) {
+        PyErr_SetString(PyExc_RuntimeError, errmsg);
+        Py_XDECREF(file);
+        return NULL;
+    }
+
     Py_XDECREF(file);
 
     if (PyErr_CheckSignals())
@@ -713,8 +715,8 @@ faulthandler_thread(void *unused)
 
         (void)_Py_write_noraise(thread.fd, thread.header, 
(int)thread.header_len);
 
-        errmsg = _Py_DumpTracebackThreads(thread.fd, thread.interp, NULL,
-                                          thread.max_threads);
+        errmsg = PyUnstable_DumpTracebackThreads(thread.fd, thread.interp, 
NULL,
+                                                 thread.max_threads);
         ok = (errmsg == NULL);
 
         if (thread.exit)
diff --git a/Platforms/emscripten/node_entry.mjs 
b/Platforms/emscripten/node_entry.mjs
index 9478b7714adbc8..110aadc5de1014 100644
--- a/Platforms/emscripten/node_entry.mjs
+++ b/Platforms/emscripten/node_entry.mjs
@@ -57,6 +57,6 @@ try {
   // Show JavaScript exception and traceback
   console.warn(e);
   // Show Python exception and traceback
-  Module.__Py_DumpTraceback(2, Module._PyGILState_GetThisThreadState());
+  Module.PyUnstable_DumpTraceback(2, Module._PyGILState_GetThisThreadState());
   process.exit(1);
 }
diff --git a/Platforms/emscripten/web_example_pyrepl_jspi/src.mjs 
b/Platforms/emscripten/web_example_pyrepl_jspi/src.mjs
index 5642372c9d2472..38a622117c2a50 100644
--- a/Platforms/emscripten/web_example_pyrepl_jspi/src.mjs
+++ b/Platforms/emscripten/web_example_pyrepl_jspi/src.mjs
@@ -189,6 +189,6 @@ try {
   // Show JavaScript exception and traceback
   console.warn(e);
   // Show Python exception and traceback
-  Module.__Py_DumpTraceback(2, Module._PyGILState_GetThisThreadState());
+  Module.PyUnstable_DumpTraceback(2, Module._PyGILState_GetThisThreadState());
   process.exit(1);
 }
diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c
index 728c0acdd4df67..8f31756f3df840 100644
--- a/Python/pylifecycle.c
+++ b/Python/pylifecycle.c
@@ -29,7 +29,7 @@
 #include "pycore_setobject.h"     // _PySet_NextEntry()
 #include "pycore_stats.h"         // _PyStats_InterpInit()
 #include "pycore_sysmodule.h"     // _PySys_ClearAttrString()
-#include "pycore_traceback.h"     // _Py_DumpTracebackThreads()
+#include "pycore_traceback.h"     // PyUnstable_TracebackThreads()
 #include "pycore_tuple.h"         // _PyTuple_FromPair
 #include "pycore_typeobject.h"    // _PyTypes_InitTypes()
 #include "pycore_typevarobject.h" // _Py_clear_generic_types()
@@ -3348,9 +3348,9 @@ _Py_FatalError_DumpTracebacks(int fd, PyInterpreterState 
*interp,
 
     /* display the current Python stack */
 #ifndef Py_GIL_DISABLED
-    _Py_DumpTracebackThreads(fd, interp, tstate, 0);
+    PyUnstable_DumpTracebackThreads(fd, interp, tstate, 0);
 #else
-    _Py_DumpTraceback(fd, tstate);
+    PyUnstable_DumpTraceback(fd, tstate);
 #endif
 }
 
diff --git a/Python/traceback.c b/Python/traceback.c
index f0e0df7101bc21..50a79d78d2e10e 100644
--- a/Python/traceback.c
+++ b/Python/traceback.c
@@ -1167,10 +1167,11 @@ dump_traceback(int fd, PyThreadState *tstate, int 
write_header)
 
    The caller is responsible to call PyErr_CheckSignals() to call Python signal
    handlers if signals were received. */
-void
-_Py_DumpTraceback(int fd, PyThreadState *tstate)
+const char*
+PyUnstable_DumpTraceback(int fd, PyThreadState *tstate)
 {
     dump_traceback(fd, tstate, 1);
+    return NULL;
 }
 
 #if defined(HAVE_PTHREAD_GETNAME_NP) || defined(HAVE_PTHREAD_GET_NAME_NP)
@@ -1264,16 +1265,16 @@ write_thread_id(int fd, PyThreadState *tstate, int 
is_current)
    The caller is responsible to call PyErr_CheckSignals() to call Python signal
    handlers if signals were received. */
 const char* _Py_NO_SANITIZE_THREAD
-_Py_DumpTracebackThreads(int fd, PyInterpreterState *interp,
-                         PyThreadState *current_tstate,
-                         Py_ssize_t max_threads)
+PyUnstable_DumpTracebackThreads(int fd, PyInterpreterState *interp,
+                                PyThreadState *current_tstate,
+                                Py_ssize_t max_threads)
 {
     if (max_threads == 0) {
         max_threads = DEFAULT_MAX_NTHREADS;
     }
 
     if (current_tstate == NULL) {
-        /* _Py_DumpTracebackThreads() is called from signal handlers by
+        /* PyUnstable_DumpTracebackThreads() is called from signal handlers by
            faulthandler.
 
            SIGSEGV, SIGFPE, SIGABRT, SIGBUS and SIGILL are synchronous signals
diff --git a/configure b/configure
index 627f5ee4888a1f..d4a0a9d74b7631 100755
--- a/configure
+++ b/configure
@@ -9788,7 +9788,7 @@ fi
 
         as_fn_append LINKFORSHARED " -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js 
-lproxyfs.js -lworkerfs.js"
     as_fn_append LINKFORSHARED " 
-sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY,ERRNO_CODES"
-    as_fn_append LINKFORSHARED " 
-sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,_PyGILState_GetThisThreadState,__Py_DumpTraceback,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET"
+    as_fn_append LINKFORSHARED " 
-sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,_PyGILState_GetThisThreadState,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET"
     as_fn_append LINKFORSHARED " -sSTACK_SIZE=5MB"
         as_fn_append LINKFORSHARED " -sTEXTDECODER=2"
 
diff --git a/configure.ac b/configure.ac
index 4f968b25d91566..395bfc30651171 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2406,7 +2406,7 @@ AS_CASE([$ac_sys_system],
     dnl Include file system support
     AS_VAR_APPEND([LINKFORSHARED], [" -sFORCE_FILESYSTEM -lidbfs.js 
-lnodefs.js -lproxyfs.js -lworkerfs.js"])
     AS_VAR_APPEND([LINKFORSHARED], [" 
-sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY,ERRNO_CODES"])
-    AS_VAR_APPEND([LINKFORSHARED], [" 
-sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,_PyGILState_GetThisThreadState,__Py_DumpTraceback,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET"])
+    AS_VAR_APPEND([LINKFORSHARED], [" 
-sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,_PyGILState_GetThisThreadState,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET"])
     AS_VAR_APPEND([LINKFORSHARED], [" -sSTACK_SIZE=5MB"])
     dnl Avoid bugs in JS fallback string decoding path
     AS_VAR_APPEND([LINKFORSHARED], [" -sTEXTDECODER=2"])

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to