Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ 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>`__.


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
17 changes: 17 additions & 0 deletions pythoncapi_compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -2659,6 +2659,23 @@ 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)
{
assert(op != NULL);
if (!PyUnstable_Object_IsUniquelyReferenced(op) || PyUnicode_Check(op)) {
return 0;
}
_Py_SetImmortal(op);
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 0x030E0000 <= 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 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}
};

Expand Down
Loading