Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ Python 3.15

On PyPy, always returns ``-1``.

.. c:function:: int PyUnstable_SetImmortal(PyObject *op)

See `PyUnstable_SetImmortal() documentation <https://docs.python.org/dev/c-api/object.html#c.PyUnstable_SetImmortal>`__.

Availability: Python 3.13 and newer, not available on PyPy.

Python 3.14
-----------
Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Changelog
=========

* 2026-02-12: Add functions:

* ``PyUnstable_SetImmortal()``

* 2025-10-14: Add functions:

* ``PyTuple_FromArray()``
Expand Down
31 changes: 31 additions & 0 deletions pythoncapi_compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -2659,6 +2659,37 @@ PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op)
}
#endif

#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 using _Py_SetImmortal() and uses private macros on 3.13.
static inline int
PyUnstable_SetImmortal(PyObject *op)
{
assert(op != NULL);
if (!PyUnstable_Object_IsUniquelyReferenced(op) || PyUnicode_Check(op)) {
return 0;
}
#if 0x030E0000 <= PY_VERSION_HEX
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)) {
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

#ifdef __cplusplus
}
Expand Down
36 changes: 36 additions & 0 deletions tests/test_pythoncapi_compat_cext.c
Original file line number Diff line number Diff line change
Expand Up @@ -2489,6 +2489,39 @@ test_try_incref(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
Py_RETURN_NONE;
}

#if 0x030D0000 <= 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},
Expand Down Expand Up @@ -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 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}
};

Expand Down
Loading