From a57ea4eb936ff7c87696f12a8c996199d8834653 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 12 Feb 2026 17:30:22 +0530 Subject: [PATCH 1/8] Add PyUnstable_SetImmortal --- pythoncapi_compat.h | 13 +++++++++++ tests/test_pythoncapi_compat_cext.c | 36 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index cdfdafa..389af8b 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2659,6 +2659,19 @@ PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) } #endif +#if 0x030C0000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030F00A7 && !defined(PYPY_VERSION) +extern void _Py_SetImmortal(PyObject *op); +static inline int +PyUnstable_SetImmortal(PyObject *op) +{ + assert(op != NULL); + if (!PyUnstable_Object_IsUniquelyReferenced(op) || PyUnicode_Check(op)) { + return 0; + } + _Py_SetImmortal(op); + return 1; +} +#endif #ifdef __cplusplus } diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index e8ada23..9b697ae 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2489,6 +2489,39 @@ test_try_incref(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) Py_RETURN_NONE; } +#if 0x030C0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) +static PyObject * +test_set_immortal(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) +{ + PyObject object; + memset(&object, 0, sizeof(PyObject)); +#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; + + int rc = PyUnstable_SetImmortal(&object); + assert(rc == 1); + Py_DECREF(&object); // should not dealloc + + // Check already immortal object + rc = PyUnstable_SetImmortal(&object); + assert(rc == 0); + + // Check unicode objects + PyObject *unicode = PyUnicode_FromString("test"); + rc = PyUnstable_SetImmortal(unicode); + assert(rc == 0); + Py_DECREF(unicode); + Py_RETURN_NONE; +} +#endif + static struct PyMethodDef methods[] = { {"test_object", test_object, METH_NOARGS, _Py_NULL}, @@ -2546,6 +2579,9 @@ static struct PyMethodDef methods[] = { {"test_byteswriter", test_byteswriter, METH_NOARGS, _Py_NULL}, {"test_tuple", test_tuple, METH_NOARGS, _Py_NULL}, {"test_try_incref", test_try_incref, METH_NOARGS, _Py_NULL}, +#if 0x030C0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) + {"test_set_immortal", test_set_immortal, METH_NOARGS, _Py_NULL}, +#endif {_Py_NULL, _Py_NULL, 0, _Py_NULL} }; From 36c4507d1773787a63d295a859841373d3764573 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 12 Feb 2026 17:47:53 +0530 Subject: [PATCH 2/8] restrict to 3.14 --- pythoncapi_compat.h | 2 +- tests/test_pythoncapi_compat_cext.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 389af8b..b2bf6a0 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2659,7 +2659,7 @@ PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) } #endif -#if 0x030C0000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030F00A7 && !defined(PYPY_VERSION) +#if 0x030E0000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030F00A7 && !defined(PYPY_VERSION) extern void _Py_SetImmortal(PyObject *op); static inline int PyUnstable_SetImmortal(PyObject *op) diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 9b697ae..6cbf221 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2489,7 +2489,7 @@ test_try_incref(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) Py_RETURN_NONE; } -#if 0x030C0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) +#if 0x030E0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) static PyObject * test_set_immortal(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { @@ -2579,7 +2579,7 @@ static struct PyMethodDef methods[] = { {"test_byteswriter", test_byteswriter, METH_NOARGS, _Py_NULL}, {"test_tuple", test_tuple, METH_NOARGS, _Py_NULL}, {"test_try_incref", test_try_incref, METH_NOARGS, _Py_NULL}, -#if 0x030C0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) +#if 0x030E0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) {"test_set_immortal", test_set_immortal, METH_NOARGS, _Py_NULL}, #endif {_Py_NULL, _Py_NULL, 0, _Py_NULL} From c201d2976a89961ed4fb2a548aeec11c33fa3889 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 12 Feb 2026 17:54:41 +0530 Subject: [PATCH 3/8] add docs --- docs/api.rst | 4 ++++ docs/changelog.rst | 4 ++++ pythoncapi_compat.h | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 3f30e53..1b5bca4 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -70,6 +70,10 @@ Python 3.15 On PyPy, always returns ``-1``. +.. c:function:: int PyUnstable_SetImmortal(PyObject *op) + + See `PyUnstable_SetImmortal() documentation `__. + Python 3.14 ----------- diff --git a/docs/changelog.rst b/docs/changelog.rst index 007a665..c68d88b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,10 @@ Changelog ========= +* 2026-02-12: Add functions: + + * ``PyUnstable_SetImmortal()`` + * 2025-10-14: Add functions: * ``PyTuple_FromArray()`` diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index b2bf6a0..70552cf 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2660,6 +2660,10 @@ PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) #endif #if 0x030E0000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030F00A7 && !defined(PYPY_VERSION) +// Immortal objects were implemented in Python 3.12, however there is no easy API +// to make objects immortal until 3.14 which has _Py_SetImmortal(). Since +// immortal objects are primarily needed for free-threading, this API is implemented +// for 3.14 and above. extern void _Py_SetImmortal(PyObject *op); static inline int PyUnstable_SetImmortal(PyObject *op) From e2bac982202d987daae2fd66a0e0d91682eb79da Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 12 Feb 2026 18:11:52 +0530 Subject: [PATCH 4/8] Update pythoncapi_compat.h Co-authored-by: Victor Stinner --- pythoncapi_compat.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 70552cf..fe8b69b 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2664,10 +2664,11 @@ PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) // to make objects immortal until 3.14 which has _Py_SetImmortal(). Since // immortal objects are primarily needed for free-threading, this API is implemented // for 3.14 and above. -extern void _Py_SetImmortal(PyObject *op); static inline int PyUnstable_SetImmortal(PyObject *op) { + PyAPI_FUNC(void) _Py_SetImmortal(PyObject *op); + assert(op != NULL); if (!PyUnstable_Object_IsUniquelyReferenced(op) || PyUnicode_Check(op)) { return 0; From 9103321d0b83feee023c497157d51ef3809a8da9 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 12 Feb 2026 18:12:45 +0530 Subject: [PATCH 5/8] add availability --- docs/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api.rst b/docs/api.rst index 1b5bca4..5c43c40 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -74,6 +74,7 @@ Python 3.15 See `PyUnstable_SetImmortal() documentation `__. + Availability: Python 3.14 and newer, not available on PyPy. Python 3.14 ----------- From 3ccded6f74e5693a7e5a0b55c0e9931869eb4875 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 12 Feb 2026 18:53:24 +0530 Subject: [PATCH 6/8] add support for 3.13 --- docs/api.rst | 2 +- pythoncapi_compat.h | 20 +++++++++++++++++--- tests/test_pythoncapi_compat_cext.c | 4 ++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 5c43c40..2b61a8d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -74,7 +74,7 @@ Python 3.15 See `PyUnstable_SetImmortal() documentation `__. - Availability: Python 3.14 and newer, not available on PyPy. + Availability: Python 3.13 and newer, not available on PyPy. Python 3.14 ----------- diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index fe8b69b..e117071 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2659,11 +2659,11 @@ PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) } #endif -#if 0x030E0000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030F00A7 && !defined(PYPY_VERSION) +#if 0x030D0000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030F00A7 && !defined(PYPY_VERSION) // Immortal objects were implemented in Python 3.12, however there is no easy API // to make objects immortal until 3.14 which has _Py_SetImmortal(). Since // immortal objects are primarily needed for free-threading, this API is implemented -// for 3.14 and above. +// for 3.14 using _Py_SetImmortal() and uses private macros on 3.13. static inline int PyUnstable_SetImmortal(PyObject *op) { @@ -2673,7 +2673,21 @@ PyUnstable_SetImmortal(PyObject *op) if (!PyUnstable_Object_IsUniquelyReferenced(op) || PyUnicode_Check(op)) { return 0; } - _Py_SetImmortal(op); +#if 0x030E0000 <= PY_VERSION_HEX + _Py_SetImmortal(op); +#else + // Python 3.13 doesn't export _Py_SetImmortal() function + if (PyObject_GC_IsTracked(op)) { + PyObject_GC_UnTrack(op); + } +#ifdef Py_GIL_DISABLED + op->ob_tid = _Py_UNOWNED_TID; + op->ob_ref_local = _Py_IMMORTAL_REFCNT_LOCAL; + op->ob_ref_shared = 0; +#else + op->ob_refcnt = _Py_IMMORTAL_REFCNT; +#endif +#endif return 1; } #endif diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 6cbf221..6fd60b2 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -2489,7 +2489,7 @@ test_try_incref(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) Py_RETURN_NONE; } -#if 0x030E0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) +#if 0x030D0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) static PyObject * test_set_immortal(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) { @@ -2579,7 +2579,7 @@ static struct PyMethodDef methods[] = { {"test_byteswriter", test_byteswriter, METH_NOARGS, _Py_NULL}, {"test_tuple", test_tuple, METH_NOARGS, _Py_NULL}, {"test_try_incref", test_try_incref, METH_NOARGS, _Py_NULL}, -#if 0x030E0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) +#if 0x030D0000 <= PY_VERSION_HEX && !defined(PYPY_VERSION) {"test_set_immortal", test_set_immortal, METH_NOARGS, _Py_NULL}, #endif {_Py_NULL, _Py_NULL, 0, _Py_NULL} From 0280559ecdad5db272074cc62214cdb991e1feed Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 12 Feb 2026 18:54:34 +0530 Subject: [PATCH 7/8] fix 3.14 --- pythoncapi_compat.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index e117071..c47c89b 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2667,7 +2667,9 @@ PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) static inline int PyUnstable_SetImmortal(PyObject *op) { +#if 0x030E0000 <= PY_VERSION_HEX PyAPI_FUNC(void) _Py_SetImmortal(PyObject *op); +#else assert(op != NULL); if (!PyUnstable_Object_IsUniquelyReferenced(op) || PyUnicode_Check(op)) { From 3eae7cbcc9c3f1b45c568f63f58a3ece629fc0ed Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 12 Feb 2026 18:55:27 +0530 Subject: [PATCH 8/8] fmt --- pythoncapi_compat.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index c47c89b..e26185c 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -2667,16 +2667,13 @@ PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) static inline int PyUnstable_SetImmortal(PyObject *op) { -#if 0x030E0000 <= PY_VERSION_HEX - PyAPI_FUNC(void) _Py_SetImmortal(PyObject *op); -#else - assert(op != NULL); if (!PyUnstable_Object_IsUniquelyReferenced(op) || PyUnicode_Check(op)) { return 0; } #if 0x030E0000 <= PY_VERSION_HEX - _Py_SetImmortal(op); + PyAPI_FUNC(void) _Py_SetImmortal(PyObject *op); + _Py_SetImmortal(op); #else // Python 3.13 doesn't export _Py_SetImmortal() function if (PyObject_GC_IsTracked(op)) {