https://github.com/python/cpython/commit/600f3feb234219c9a9998e30ea653a2afb1f8116
commit: 600f3feb234219c9a9998e30ea653a2afb1f8116
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2025-11-18T16:13:13Z
summary:

gh-141070: Add PyUnstable_Object_Dump() function (#141072)

* Promote _PyObject_Dump() as a public function.
* Keep _PyObject_Dump() alias to PyUnstable_Object_Dump()
  for backward compatibility.
* Replace _PyObject_Dump() with PyUnstable_Object_Dump().

Co-authored-by: Peter Bierma <[email protected]>
Co-authored-by: Kumar Aditya <[email protected]>
Co-authored-by: Petr Viktorin <[email protected]>

files:
A Misc/NEWS.d/next/C_API/2025-11-05-21-48-31.gh-issue-141070.mkrhjQ.rst
M Doc/c-api/object.rst
M Doc/whatsnew/3.15.rst
M Include/cpython/object.h
M Include/internal/pycore_global_objects_fini_generated.h
M Lib/test/test_capi/test_object.py
M Modules/_testcapi/object.c
M Objects/object.c
M Objects/unicodeobject.c
M Python/gc.c
M Python/pythonrun.c

diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst
index 96353266ac7300..76971c46c1696b 100644
--- a/Doc/c-api/object.rst
+++ b/Doc/c-api/object.rst
@@ -85,6 +85,35 @@ Object Protocol
    instead of the :func:`repr`.
 
 
+.. c:function:: void PyUnstable_Object_Dump(PyObject *op)
+
+   Dump an object *op* to ``stderr``. This should only be used for debugging.
+
+   The output is intended to try dumping objects even after memory corruption:
+
+   * Information is written starting with fields that are the least likely to
+     crash when accessed.
+   * This function can be called without an :term:`attached thread state`, but
+     it's not recommended to do so: it can cause deadlocks.
+   * An object that does not belong to the current interpreter may be dumped,
+     but this may also cause crashes or unintended behavior.
+   * Implement a heuristic to detect if the object memory has been freed. Don't
+     display the object contents in this case, only its memory address.
+   * The output format may change at any time.
+
+   Example of output:
+
+   .. code-block:: output
+
+       object address  : 0x7f80124702c0
+       object refcount : 2
+       object type     : 0x9902e0
+       object type name: str
+       object repr     : 'abcdef'
+
+   .. versionadded:: next
+
+
 .. c:function:: int PyObject_HasAttrWithError(PyObject *o, PyObject *attr_name)
 
    Returns ``1`` if *o* has the attribute *attr_name*, and ``0`` otherwise.
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 24cc7e2d7eb911..5a98297d3f8847 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -1084,19 +1084,23 @@ New features
 
   (Contributed by Victor Stinner in :gh:`129813`.)
 
+* Add a new :c:func:`PyImport_CreateModuleFromInitfunc` C-API for creating
+  a module from a *spec* and *initfunc*.
+  (Contributed by Itamar Oren in :gh:`116146`.)
+
 * Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array.
   (Contributed by Victor Stinner in :gh:`111489`.)
 
+* Add :c:func:`PyUnstable_Object_Dump` to dump an object to ``stderr``.
+  It should only be used for debugging.
+  (Contributed by Victor Stinner in :gh:`141070`.)
+
 * Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and
   :c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set
   the stack protection base address and stack protection size of a Python
   thread state.
   (Contributed by Victor Stinner in :gh:`139653`.)
 
-* Add a new :c:func:`PyImport_CreateModuleFromInitfunc` C-API for creating
-  a module from a *spec* and *initfunc*.
-  (Contributed by Itamar Oren in :gh:`116146`.)
-
 
 Changed C APIs
 --------------
diff --git a/Include/cpython/object.h b/Include/cpython/object.h
index d64298232e705c..130a105de42150 100644
--- a/Include/cpython/object.h
+++ b/Include/cpython/object.h
@@ -295,7 +295,10 @@ PyAPI_FUNC(PyObject *) PyType_GetDict(PyTypeObject *);
 
 PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int);
 PyAPI_FUNC(void) _Py_BreakPoint(void);
-PyAPI_FUNC(void) _PyObject_Dump(PyObject *);
+PyAPI_FUNC(void) PyUnstable_Object_Dump(PyObject *);
+
+// Alias for backward compatibility
+#define _PyObject_Dump PyUnstable_Object_Dump
 
 PyAPI_FUNC(PyObject*) _PyObject_GetAttrId(PyObject *, _Py_Identifier *);
 
@@ -387,10 +390,11 @@ PyAPI_FUNC(PyObject *) _PyObject_FunctionStr(PyObject *);
    process with a message on stderr if the given condition fails to hold,
    but compile away to nothing if NDEBUG is defined.
 
-   However, before aborting, Python will also try to call _PyObject_Dump() on
-   the given object.  This may be of use when investigating bugs in which a
-   particular object is corrupt (e.g. buggy a tp_visit method in an extension
-   module breaking the garbage collector), to help locate the broken objects.
+   However, before aborting, Python will also try to call
+   PyUnstable_Object_Dump() on the given object. This may be of use when
+   investigating bugs in which a particular object is corrupt (e.g. buggy a
+   tp_visit method in an extension module breaking the garbage collector), to
+   help locate the broken objects.
 
    The WITH_MSG variant allows you to supply an additional message that Python
    will attempt to print to stderr, after the object dump. */
diff --git a/Include/internal/pycore_global_objects_fini_generated.h 
b/Include/internal/pycore_global_objects_fini_generated.h
index ecef4364cc32df..c3968aff8f3b8d 100644
--- a/Include/internal/pycore_global_objects_fini_generated.h
+++ b/Include/internal/pycore_global_objects_fini_generated.h
@@ -13,7 +13,7 @@ static inline void
 _PyStaticObject_CheckRefcnt(PyObject *obj) {
     if (!_Py_IsImmortal(obj)) {
         fprintf(stderr, "Immortal Object has less refcnt than expected.\n");
-        _PyObject_Dump(obj);
+        PyUnstable_Object_Dump(obj);
     }
 }
 #endif
diff --git a/Lib/test/test_capi/test_object.py 
b/Lib/test/test_capi/test_object.py
index d4056727d07fbf..c5040913e9e1f1 100644
--- a/Lib/test/test_capi/test_object.py
+++ b/Lib/test/test_capi/test_object.py
@@ -1,4 +1,5 @@
 import enum
+import os
 import sys
 import textwrap
 import unittest
@@ -13,6 +14,9 @@
 _testcapi = import_helper.import_module('_testcapi')
 _testinternalcapi = import_helper.import_module('_testinternalcapi')
 
+NULL = None
+STDERR_FD = 2
+
 
 class Constant(enum.IntEnum):
     Py_CONSTANT_NONE = 0
@@ -247,5 +251,53 @@ def func(x):
 
         func(object())
 
+    def pyobject_dump(self, obj, release_gil=False):
+        pyobject_dump = _testcapi.pyobject_dump
+
+        try:
+            old_stderr = os.dup(STDERR_FD)
+        except OSError as exc:
+            # os.dup(STDERR_FD) is not supported on WASI
+            self.skipTest(f"os.dup() failed with {exc!r}")
+
+        filename = os_helper.TESTFN
+        try:
+            try:
+                with open(filename, "wb") as fp:
+                    fd = fp.fileno()
+                    os.dup2(fd, STDERR_FD)
+                    pyobject_dump(obj, release_gil)
+            finally:
+                os.dup2(old_stderr, STDERR_FD)
+                os.close(old_stderr)
+
+            with open(filename) as fp:
+                return fp.read().rstrip()
+        finally:
+            os_helper.unlink(filename)
+
+    def test_pyobject_dump(self):
+        # test string object
+        str_obj = 'test string'
+        output = self.pyobject_dump(str_obj)
+        hex_regex = r'(0x)?[0-9a-fA-F]+'
+        regex = (
+            fr"object address  : {hex_regex}\n"
+             r"object refcount : [0-9]+\n"
+            fr"object type     : {hex_regex}\n"
+             r"object type name: str\n"
+             r"object repr     : 'test string'"
+        )
+        self.assertRegex(output, regex)
+
+        # release the GIL
+        output = self.pyobject_dump(str_obj, release_gil=True)
+        self.assertRegex(output, regex)
+
+        # test NULL object
+        output = self.pyobject_dump(NULL)
+        self.assertRegex(output, r'<object at .* is freed>')
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git 
a/Misc/NEWS.d/next/C_API/2025-11-05-21-48-31.gh-issue-141070.mkrhjQ.rst 
b/Misc/NEWS.d/next/C_API/2025-11-05-21-48-31.gh-issue-141070.mkrhjQ.rst
new file mode 100644
index 00000000000000..39cfcf73404ebf
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-11-05-21-48-31.gh-issue-141070.mkrhjQ.rst
@@ -0,0 +1,2 @@
+Add :c:func:`PyUnstable_Object_Dump` to dump an object to ``stderr``. It should
+only be used for debugging. Patch by Victor Stinner.
diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c
index 798ef97c495aeb..4c9632c07a99f4 100644
--- a/Modules/_testcapi/object.c
+++ b/Modules/_testcapi/object.c
@@ -485,6 +485,30 @@ is_uniquely_referenced(PyObject *self, PyObject *op)
 }
 
 
+static PyObject *
+pyobject_dump(PyObject *self, PyObject *args)
+{
+    PyObject *op;
+    int release_gil = 0;
+
+    if (!PyArg_ParseTuple(args, "O|i", &op, &release_gil)) {
+        return NULL;
+    }
+    NULLABLE(op);
+
+    if (release_gil) {
+        Py_BEGIN_ALLOW_THREADS
+        PyUnstable_Object_Dump(op);
+        Py_END_ALLOW_THREADS
+
+    }
+    else {
+        PyUnstable_Object_Dump(op);
+    }
+    Py_RETURN_NONE;
+}
+
+
 static PyMethodDef test_methods[] = {
     {"call_pyobject_print", call_pyobject_print, METH_VARARGS},
     {"pyobject_print_null", pyobject_print_null, METH_VARARGS},
@@ -511,6 +535,7 @@ static PyMethodDef test_methods[] = {
     {"test_py_is_funcs", test_py_is_funcs, METH_NOARGS},
     {"clear_managed_dict", clear_managed_dict, METH_O, NULL},
     {"is_uniquely_referenced", is_uniquely_referenced, METH_O},
+    {"pyobject_dump", pyobject_dump, METH_VARARGS},
     {NULL},
 };
 
diff --git a/Objects/object.c b/Objects/object.c
index 0540112d7d2acf..0a80c6edcf158c 100644
--- a/Objects/object.c
+++ b/Objects/object.c
@@ -713,7 +713,7 @@ _PyObject_IsFreed(PyObject *op)
 
 /* For debugging convenience.  See Misc/gdbinit for some useful gdb hooks */
 void
-_PyObject_Dump(PyObject* op)
+PyUnstable_Object_Dump(PyObject* op)
 {
     if (_PyObject_IsFreed(op)) {
         /* It seems like the object memory has been freed:
@@ -3150,7 +3150,7 @@ _PyObject_AssertFailed(PyObject *obj, const char *expr, 
const char *msg,
 
         /* This might succeed or fail, but we're about to abort, so at least
            try to provide any extra info we can: */
-        _PyObject_Dump(obj);
+        PyUnstable_Object_Dump(obj);
 
         fprintf(stderr, "\n");
         fflush(stderr);
diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c
index 4e8c132327b7d0..7f9f75126a9e56 100644
--- a/Objects/unicodeobject.c
+++ b/Objects/unicodeobject.c
@@ -547,7 +547,8 @@ unicode_check_encoding_errors(const char *encoding, const 
char *errors)
     }
 
     /* Disable checks during Python finalization. For example, it allows to
-       call _PyObject_Dump() during finalization for debugging purpose. */
+     * call PyUnstable_Object_Dump() during finalization for debugging purpose.
+     */
     if (_PyInterpreterState_GetFinalizing(interp) != NULL) {
         return 0;
     }
diff --git a/Python/gc.c b/Python/gc.c
index 064f9406e0a17c..27364ecfdcd5c6 100644
--- a/Python/gc.c
+++ b/Python/gc.c
@@ -2237,7 +2237,7 @@ _PyGC_Fini(PyInterpreterState *interp)
 void
 _PyGC_Dump(PyGC_Head *g)
 {
-    _PyObject_Dump(FROM_GC(g));
+    PyUnstable_Object_Dump(FROM_GC(g));
 }
 
 
diff --git a/Python/pythonrun.c b/Python/pythonrun.c
index 49ce0a97d4742f..272be504a68fa1 100644
--- a/Python/pythonrun.c
+++ b/Python/pythonrun.c
@@ -1181,7 +1181,7 @@ _PyErr_Display(PyObject *file, PyObject *unused, PyObject 
*value, PyObject *tb)
     }
     if (print_exception_recursive(&ctx, value) < 0) {
         PyErr_Clear();
-        _PyObject_Dump(value);
+        PyUnstable_Object_Dump(value);
         fprintf(stderr, "lost sys.stderr\n");
     }
     Py_XDECREF(ctx.seen);
@@ -1199,14 +1199,14 @@ PyErr_Display(PyObject *unused, PyObject *value, 
PyObject *tb)
     PyObject *file;
     if (PySys_GetOptionalAttr(&_Py_ID(stderr), &file) < 0) {
         PyObject *exc = PyErr_GetRaisedException();
-        _PyObject_Dump(value);
+        PyUnstable_Object_Dump(value);
         fprintf(stderr, "lost sys.stderr\n");
-        _PyObject_Dump(exc);
+        PyUnstable_Object_Dump(exc);
         Py_DECREF(exc);
         return;
     }
     if (file == NULL) {
-        _PyObject_Dump(value);
+        PyUnstable_Object_Dump(value);
         fprintf(stderr, "lost sys.stderr\n");
         return;
     }

_______________________________________________
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