From b874f0b43a717205c57e26020254aa46ec9d406b Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 6 Feb 2026 19:11:20 +0530 Subject: [PATCH 01/10] implement PyUnstable_SetImmortal --- Doc/c-api/object.rst | 16 ++++++++++++++++ Include/cpython/object.h | 2 ++ Modules/_testcapi/object.c | 18 ++++++++++++++++++ Objects/object.c | 8 ++++++++ 4 files changed, 44 insertions(+) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 992a4383f97241..476776e32f9b34 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -801,3 +801,19 @@ Object Protocol cannot fail. .. versionadded:: 3.14 + +.. c:function:: int PyUnstable_SetImmortal(PyObject *obj) + + Marks the object op immortal. The argument should be uniquely referenced by + the calling thread. + + This is a one-way process: objects can only be made immortal, they cannot be + made mortal once again. Immortal objects do not participate in reference counting + and will never be garbage collected. + + This function is intended to be used soon after op is created, by the code that + creates it, such as in the object's tp_new slot. + Returns 1 if the object was made immortal and returns 0 if it was not. + This function cannot fail. + + .. versionadded:: next \ No newline at end of file diff --git a/Include/cpython/object.h b/Include/cpython/object.h index 28c909531dba64..61cdb93d1d5354 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -493,3 +493,5 @@ PyAPI_FUNC(int) PyUnstable_TryIncRef(PyObject *); PyAPI_FUNC(void) PyUnstable_EnableTryIncRef(PyObject *); PyAPI_FUNC(int) PyUnstable_Object_IsUniquelyReferenced(PyObject *); + +PyAPI_FUNC(int) PyUnstable_SetImmortal(PyObject *op); diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index 153b28e5fe2e95..8b67e79b1f090b 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -201,6 +201,23 @@ test_py_try_inc_ref(PyObject *self, PyObject *unused) Py_RETURN_NONE; } +static PyObject * +test_py_set_immortal(PyObject *self, PyObject *unused) +{ + // the object is allocated on C stack as otherwise, + // it would trip the refleak checker when the object + // is made immortal and leak memory + PyObject object = { + .ob_refcnt = 1, + .ob_type = &PyBaseObject_Type, + }; + assert(!PyUnstable_IsImmortal(&object)); + PyUnstable_SetImmortal(&object); + assert(PyUnstable_IsImmortal(&object)); + Py_DECREF(&object); // should not dealloc + assert(PyUnstable_IsImmortal(&object)); + Py_RETURN_NONE; +} static PyObject * _test_incref(PyObject *ob) @@ -528,6 +545,7 @@ static PyMethodDef test_methods[] = { {"pyobject_is_unique_temporary", pyobject_is_unique_temporary, METH_O}, {"pyobject_is_unique_temporary_new_object", pyobject_is_unique_temporary_new_object, METH_NOARGS}, {"test_py_try_inc_ref", test_py_try_inc_ref, METH_NOARGS}, + {"test_py_set_immortal", test_py_set_immortal, METH_NOARGS}, {"test_xincref_doesnt_leak",test_xincref_doesnt_leak, METH_NOARGS}, {"test_incref_doesnt_leak", test_incref_doesnt_leak, METH_NOARGS}, {"test_xdecref_doesnt_leak",test_xdecref_doesnt_leak, METH_NOARGS}, diff --git a/Objects/object.c b/Objects/object.c index 38717def24239f..53bf405b96fcd2 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -2790,6 +2790,14 @@ PyUnstable_EnableTryIncRef(PyObject *op) #endif } +int +PyUnstable_SetImmortal(PyObject *op) +{ + assert(op != NULL); + _Py_SetImmortal(op); + return 1; +} + void _Py_ResurrectReference(PyObject *op) { From cb6cc99349a4d4993623cee265fef98f879d2942 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 6 Feb 2026 19:21:36 +0530 Subject: [PATCH 02/10] fix compilation under ft --- Modules/_testcapi/object.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index 8b67e79b1f090b..55ef5be573e8e5 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -207,10 +207,9 @@ test_py_set_immortal(PyObject *self, PyObject *unused) // the object is allocated on C stack as otherwise, // it would trip the refleak checker when the object // is made immortal and leak memory - PyObject object = { - .ob_refcnt = 1, - .ob_type = &PyBaseObject_Type, - }; + PyObject object = {0}; + Py_SET_REFCNT(&object, 1); + Py_SET_TYPE(&object, &PyBaseObject_Type); assert(!PyUnstable_IsImmortal(&object)); PyUnstable_SetImmortal(&object); assert(PyUnstable_IsImmortal(&object)); From c897a08d2eca94186518e7f58fee096b0ae946fb Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sat, 7 Feb 2026 08:47:00 +0530 Subject: [PATCH 03/10] minor fixes --- Doc/c-api/object.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 476776e32f9b34..12a98fd40f7523 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -802,16 +802,16 @@ Object Protocol .. versionadded:: 3.14 -.. c:function:: int PyUnstable_SetImmortal(PyObject *obj) +.. c:function:: int PyUnstable_SetImmortal(PyObject *op) - Marks the object op immortal. The argument should be uniquely referenced by + Marks the object *op* immortal. The argument should be uniquely referenced by the calling thread. This is a one-way process: objects can only be made immortal, they cannot be made mortal once again. Immortal objects do not participate in reference counting and will never be garbage collected. - This function is intended to be used soon after op is created, by the code that + This function is intended to be used soon after *op* is created, by the code that creates it, such as in the object's tp_new slot. Returns 1 if the object was made immortal and returns 0 if it was not. This function cannot fail. From f00e23f8364057b7b30683b22140165c8694f7de Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 10 Feb 2026 19:02:56 +0530 Subject: [PATCH 04/10] Apply suggestions from code review Co-authored-by: Peter Bierma --- Doc/c-api/object.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 12a98fd40f7523..aa36934a4e0d00 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -804,15 +804,15 @@ Object Protocol .. c:function:: int PyUnstable_SetImmortal(PyObject *op) - Marks the object *op* immortal. The argument should be uniquely referenced by + Marks the object *op* :term:`immortal`. The argument should be uniquely referenced by the calling thread. - This is a one-way process: objects can only be made immortal, they cannot be + This is a one-way process: objects can only be made immortal; they cannot be made mortal once again. Immortal objects do not participate in reference counting and will never be garbage collected. This function is intended to be used soon after *op* is created, by the code that - creates it, such as in the object's tp_new slot. + creates it, such as in the object's :c:member:`~PyTypeObject.tp_new` slot. Returns 1 if the object was made immortal and returns 0 if it was not. This function cannot fail. From f42377aa3a06cc8ca0a646424f7bf83d0e9b8d73 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 10 Feb 2026 19:03:30 +0530 Subject: [PATCH 05/10] address review --- Objects/object.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Objects/object.c b/Objects/object.c index 53bf405b96fcd2..07c1760259493d 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -2794,6 +2794,9 @@ int PyUnstable_SetImmortal(PyObject *op) { assert(op != NULL); + if (!_PyObject_IsUniquelyReferenced(op) || PyUnicode_Check(op)) { + return 0; + } _Py_SetImmortal(op); return 1; } From 9987a92ec5d2b60c442b367a81b493d42d75d8ad Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 10 Feb 2026 19:38:59 +0530 Subject: [PATCH 06/10] add more tests --- Modules/_testcapi/object.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index 55ef5be573e8e5..0a2d24f73ce52e 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -211,10 +211,23 @@ test_py_set_immortal(PyObject *self, PyObject *unused) Py_SET_REFCNT(&object, 1); Py_SET_TYPE(&object, &PyBaseObject_Type); assert(!PyUnstable_IsImmortal(&object)); - PyUnstable_SetImmortal(&object); + int rc = PyUnstable_SetImmortal(&object); + assert(rc == 1); assert(PyUnstable_IsImmortal(&object)); Py_DECREF(&object); // should not dealloc assert(PyUnstable_IsImmortal(&object)); + + // Check already immortal object + rc = PyUnstable_SetImmortal(&object); + assert(rc == 0); + + // Check unicode objects + PyObject *unicode = PyUnicode_FromString("test"); + assert(!PyUnstable_IsImmortal(unicode)); + rc = PyUnstable_SetImmortal(unicode); + assert(rc == 0); + assert(!PyUnstable_IsImmortal(unicode)); + Py_DECREF(unicode); Py_RETURN_NONE; } From 9a4003c610d9a5ebdb9dbe40590f1b7cf70e491a Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 10 Feb 2026 20:33:47 +0530 Subject: [PATCH 07/10] fix test on ft --- Modules/_testcapi/object.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index 0a2d24f73ce52e..9160005e00654f 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -206,10 +206,19 @@ test_py_set_immortal(PyObject *self, PyObject *unused) { // the object is allocated on C stack as otherwise, // it would trip the refleak checker when the object - // is made immortal and leak memory + // is made immortal and leak memory, for the same + // reason we cannot call PyObject_Init() on it. PyObject object = {0}; - Py_SET_REFCNT(&object, 1); - Py_SET_TYPE(&object, &PyBaseObject_Type); +#ifdef Py_GIL_DISABLED + object.ob_tid = _Py_ThreadId(); + object.ob_gc_bits = 0; + object.ob_ref_local = 1; + object.ob_ref_shared = 0; +#else + object.ob_refcnt = 1; +#endif + object.ob_type = &PyBaseObject_Type; + assert(!PyUnstable_IsImmortal(&object)); int rc = PyUnstable_SetImmortal(&object); assert(rc == 1); From 8fe0fb59776cc480cd042a3d652b05221e769b83 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 10 Feb 2026 20:35:16 +0530 Subject: [PATCH 08/10] fix lint --- Doc/c-api/object.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index aa36934a4e0d00..734a24344bd787 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -816,4 +816,4 @@ Object Protocol Returns 1 if the object was made immortal and returns 0 if it was not. This function cannot fail. - .. versionadded:: next \ No newline at end of file + .. versionadded:: next From a146befcc4038bfb26e2fefdb18356f7980b3a48 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 11 Feb 2026 18:53:31 +0530 Subject: [PATCH 09/10] address final review --- Doc/c-api/object.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 734a24344bd787..f71bfebdb2a19a 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -805,11 +805,12 @@ Object Protocol .. c:function:: int PyUnstable_SetImmortal(PyObject *op) Marks the object *op* :term:`immortal`. The argument should be uniquely referenced by - the calling thread. + the calling thread. This is intended to be used for reducing reference counting contention + in the :term:`free-threaded build` for objects which are shared across threads. This is a one-way process: objects can only be made immortal; they cannot be made mortal once again. Immortal objects do not participate in reference counting - and will never be garbage collected. + and will never be garbage collected. If the object is GC-tracked, it is untracked. This function is intended to be used soon after *op* is created, by the code that creates it, such as in the object's :c:member:`~PyTypeObject.tp_new` slot. From e1ef13d894d5f061695171c6e2357dba87a427b9 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:30:18 +0000 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-02-11-13-30-11.gh-issue-143300.yjB63-.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-02-11-13-30-11.gh-issue-143300.yjB63-.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-11-13-30-11.gh-issue-143300.yjB63-.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-11-13-30-11.gh-issue-143300.yjB63-.rst new file mode 100644 index 00000000000000..85c75a224e42fc --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-11-13-30-11.gh-issue-143300.yjB63-.rst @@ -0,0 +1 @@ +Add :c:func:`PyUnstable_SetImmortal` C-API function to mark objects as :term:`immortal`.