Skip to content
5 changes: 5 additions & 0 deletions Doc/c-api/interp-lifecycle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,11 @@ Initializing and finalizing the interpreter
(zero) if not. After :c:func:`Py_FinalizeEx` is called, this returns false until
:c:func:`Py_Initialize` is called again.
.. versionchanged:: next
This function no longer returns true until initialization has fully
completed, including import of the :mod:`site` module. Previously it
could return true while :c:func:`Py_Initialize` was still running.
Comment on lines +413 to +416
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is technically a bug fix, we don't always call these out in the main docs. i'm happy dropping this, but it feels worthwhile to mention for embedders likely to use this API. and if we choose to backport this as a bugfix it'll include proper 3.14.x and 3.13.x attribution to indicate when people need to work around it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is fine to add versionchanged when an important behavior needed to be pointed out in a bugfix release so I don't mind keeping this here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However considering what it changed I think it is best to keep it in 3.15 only.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I'm fine not backporting this. It's rare enough and PyO3 already has a workaround.

.. c:function:: int Py_IsFinalizing()
Expand Down
23 changes: 23 additions & 0 deletions Include/internal/pycore_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ _PyRuntimeState_SetFinalizing(_PyRuntimeState *runtime, PyThreadState *tstate) {
}
}

// Atomic so a thread that reads initialized=1 observes all writes
// from the initialization sequence (gh-146302).

static inline int
_PyRuntimeState_GetCoreInitialized(_PyRuntimeState *runtime) {
return _Py_atomic_load_int(&runtime->core_initialized);
}

static inline void
_PyRuntimeState_SetCoreInitialized(_PyRuntimeState *runtime, int initialized) {
_Py_atomic_store_int(&runtime->core_initialized, initialized);
}

static inline int
_PyRuntimeState_GetInitialized(_PyRuntimeState *runtime) {
return _Py_atomic_load_int(&runtime->initialized);
}

static inline void
_PyRuntimeState_SetInitialized(_PyRuntimeState *runtime, int initialized) {
_Py_atomic_store_int(&runtime->initialized, initialized);
}


#ifdef __cplusplus
}
Expand Down
12 changes: 10 additions & 2 deletions Include/internal/pycore_runtime_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,18 @@ struct pyruntimestate {
/* Is Python preinitialized? Set to 1 by Py_PreInitialize() */
int preinitialized;

/* Is Python core initialized? Set to 1 by _Py_InitializeCore() */
/* Is Python core initialized? Set to 1 by _Py_InitializeCore().
Use _PyRuntimeState_GetCoreInitialized() and
_PyRuntimeState_SetCoreInitialized() to access it,
don't access it directly. */
int core_initialized;

/* Is Python fully initialized? Set to 1 by Py_Initialize() */
/* Is Python fully initialized? Set to 1 by Py_Initialize().
Use _PyRuntimeState_GetInitialized() and
_PyRuntimeState_SetInitialized() to access it,
don't access it directly. */
int initialized;

/* Set by Py_FinalizeEx(). Only reset to NULL if Py_Initialize()
Expand Down
6 changes: 6 additions & 0 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -1930,6 +1930,12 @@ def test_init_in_background_thread(self):
out, err = self.run_embedded_interpreter("test_init_in_background_thread")
self.assertEqual(err, "")

def test_isinitialized_false_during_site_import(self):
# gh-146302: Py_IsInitialized() must not return true during site import.
out, err = self.run_embedded_interpreter(
"test_isinitialized_false_during_site_import")
self.assertEqual(err, "")


class AuditingTests(EmbeddingTestsMixin, unittest.TestCase):
def test_open_code_hook(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:c:func:`Py_IsInitialized` no longer returns true until initialization has
fully completed, including import of the :mod:`site` module. The underlying
runtime flags now use atomic operations.
47 changes: 47 additions & 0 deletions Programs/_testembed.c
Original file line number Diff line number Diff line change
Expand Up @@ -2203,6 +2203,52 @@ static int test_init_in_background_thread(void)
return PyThread_join_thread(handle);
}

/* gh-146302: Py_IsInitialized() must not return true during site import. */
static int _initialized_during_site_import = -1; /* -1 = not observed */

static int hook_check_initialized_on_site_import(
const char *event, PyObject *args, void *userData)
{
if (strcmp(event, "import") == 0 && args != NULL) {
PyObject *name = PyTuple_GetItem(args, 0);
if (name != NULL && PyUnicode_Check(name)
&& PyUnicode_CompareWithASCIIString(name, "site") == 0
&& _initialized_during_site_import == -1)
{
_initialized_during_site_import = Py_IsInitialized();
}
}
return 0;
}

static int test_isinitialized_false_during_site_import(void)
{
_initialized_during_site_import = -1;

/* Register audit hook before initialization */
PySys_AddAuditHook(hook_check_initialized_on_site_import, NULL);

_testembed_initialize();

if (_initialized_during_site_import == -1) {
error("audit hook never observed site import");
Py_Finalize();
return 1;
}
if (_initialized_during_site_import != 0) {
error("Py_IsInitialized() was true during site import");
Py_Finalize();
return 1;
}
if (!Py_IsInitialized()) {
error("Py_IsInitialized() was false after Py_Initialize()");
return 1;
}

Py_Finalize();
return 0;
}


#ifndef MS_WINDOWS
#include "test_frozenmain.h" // M_test_frozenmain
Expand Down Expand Up @@ -2693,6 +2739,7 @@ static struct TestCase TestCases[] = {
{"test_init_use_frozen_modules", test_init_use_frozen_modules},
{"test_init_main_interpreter_settings", test_init_main_interpreter_settings},
{"test_init_in_background_thread", test_init_in_background_thread},
{"test_isinitialized_false_during_site_import", test_isinitialized_false_during_site_import},

// Audit
{"test_open_code_hook", test_open_code_hook},
Expand Down
2 changes: 1 addition & 1 deletion Python/preconfig.c
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ _PyPreConfig_Write(const PyPreConfig *src_config)
return status;
}

if (_PyRuntime.core_initialized) {
if (_Py_IsCoreInitialized()) {
/* bpo-34008: Calling this functions after Py_Initialize() ignores
the new configuration. */
return _PyStatus_OK();
Expand Down
35 changes: 18 additions & 17 deletions Python/pylifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,13 @@ int (*_PyOS_mystrnicmp_hack)(const char *, const char *, Py_ssize_t) = \
int
_Py_IsCoreInitialized(void)
{
return _PyRuntime.core_initialized;
return _PyRuntimeState_GetCoreInitialized(&_PyRuntime);
}

int
Py_IsInitialized(void)
{
return _PyRuntime.initialized;
return _PyRuntimeState_GetInitialized(&_PyRuntime);
}


Expand Down Expand Up @@ -491,7 +491,7 @@ static PyStatus
pycore_init_runtime(_PyRuntimeState *runtime,
const PyConfig *config)
{
if (runtime->initialized) {
if (_PyRuntimeState_GetInitialized(runtime)) {
return _PyStatus_ERR("main interpreter already initialized");
}

Expand Down Expand Up @@ -984,7 +984,7 @@ pyinit_config(_PyRuntimeState *runtime,
}

/* Only when we get here is the runtime core fully initialized */
runtime->core_initialized = 1;
_PyRuntimeState_SetCoreInitialized(runtime, 1);
return _PyStatus_OK();
}

Expand Down Expand Up @@ -1217,7 +1217,7 @@ init_interp_main(PyThreadState *tstate)
* or pure Python code in the standard library won't work.
*/
if (is_main_interp) {
interp->runtime->initialized = 1;
_PyRuntimeState_SetInitialized(interp->runtime, 1);
}
return _PyStatus_OK();
}
Expand Down Expand Up @@ -1329,8 +1329,6 @@ init_interp_main(PyThreadState *tstate)
Py_XDECREF(warnings_module);
}
Py_XDECREF(warnoptions);

interp->runtime->initialized = 1;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this down after site and the rest of the machinery is the part I'm most interested in seeing tested. Intuitively I would not expect any code site may import to ever care about Py_IsInitialized() itself as extension module code "should" never have a reason to use that API as it shouldn't be called outside of an interpreter.

But this is the one thing in this change that might be an observable behavior change. Even if we don't agree with someone relying on the existing behavior.

}

if (config->site_import) {
Expand Down Expand Up @@ -1426,6 +1424,10 @@ init_interp_main(PyThreadState *tstate)

assert(!_PyErr_Occurred(tstate));

if (is_main_interp) {
_PyRuntimeState_SetInitialized(interp->runtime, 1);
}

return _PyStatus_OK();
}

Expand All @@ -1445,11 +1447,11 @@ static PyStatus
pyinit_main(PyThreadState *tstate)
{
PyInterpreterState *interp = tstate->interp;
if (!interp->runtime->core_initialized) {
if (!_PyRuntimeState_GetCoreInitialized(interp->runtime)) {
return _PyStatus_ERR("runtime core not initialized");
}

if (interp->runtime->initialized) {
if (_PyRuntimeState_GetInitialized(interp->runtime)) {
return pyinit_main_reconfigure(tstate);
}

Expand Down Expand Up @@ -1503,9 +1505,8 @@ Py_InitializeEx(int install_sigs)
if (_PyStatus_EXCEPTION(status)) {
Py_ExitStatusException(status);
}
_PyRuntimeState *runtime = &_PyRuntime;

if (runtime->initialized) {
if (Py_IsInitialized()) {
/* bpo-33932: Calling Py_Initialize() twice does nothing. */
return;
}
Expand Down Expand Up @@ -2210,7 +2211,7 @@ _Py_Finalize(_PyRuntimeState *runtime)
int status = 0;

/* Bail out early if already finalized (or never initialized). */
if (!runtime->initialized) {
if (!_PyRuntimeState_GetInitialized(runtime)) {
return status;
}

Expand Down Expand Up @@ -2245,8 +2246,8 @@ _Py_Finalize(_PyRuntimeState *runtime)
when they attempt to take the GIL (ex: PyEval_RestoreThread()). */
_PyInterpreterState_SetFinalizing(tstate->interp, tstate);
_PyRuntimeState_SetFinalizing(runtime, tstate);
runtime->initialized = 0;
runtime->core_initialized = 0;
_PyRuntimeState_SetInitialized(runtime, 0);
_PyRuntimeState_SetCoreInitialized(runtime, 0);

// XXX Call something like _PyImport_Disable() here?

Expand Down Expand Up @@ -2472,7 +2473,7 @@ new_interpreter(PyThreadState **tstate_p,
}
_PyRuntimeState *runtime = &_PyRuntime;

if (!runtime->initialized) {
if (!_PyRuntimeState_GetInitialized(runtime)) {
return _PyStatus_ERR("Py_Initialize must be called first");
}

Expand Down Expand Up @@ -3312,10 +3313,10 @@ fatal_error_dump_runtime(int fd, _PyRuntimeState *runtime)
_Py_DumpHexadecimal(fd, (uintptr_t)finalizing, sizeof(finalizing) * 2);
PUTS(fd, ")");
}
else if (runtime->initialized) {
else if (_PyRuntimeState_GetInitialized(runtime)) {
PUTS(fd, "initialized");
}
else if (runtime->core_initialized) {
else if (_PyRuntimeState_GetCoreInitialized(runtime)) {
PUTS(fd, "core initialized");
}
else if (runtime->preinitialized) {
Expand Down
4 changes: 2 additions & 2 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,8 @@ init_runtime(_PyRuntimeState *runtime,
{
assert(!runtime->preinitializing);
assert(!runtime->preinitialized);
assert(!runtime->core_initialized);
assert(!runtime->initialized);
assert(!_PyRuntimeState_GetCoreInitialized(runtime));
assert(!_PyRuntimeState_GetInitialized(runtime));
assert(!runtime->_initialized);

runtime->open_code_hook = open_code_hook;
Expand Down
3 changes: 2 additions & 1 deletion Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Data members:
#include "pycore_pymem.h" // _PyMem_DefaultRawFree()
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "pycore_pystats.h" // _Py_PrintSpecializationStats()
#include "pycore_runtime.h" // _PyRuntimeState_Get*()
#include "pycore_structseq.h" // _PyStructSequence_InitBuiltinWithFlags()
#include "pycore_sysmodule.h" // export _PySys_GetSizeOf()
#include "pycore_unicodeobject.h" // _PyUnicode_InternImmortal()
Expand Down Expand Up @@ -471,7 +472,7 @@ PySys_AddAuditHook(Py_AuditHookFunction hook, void *userData)
PySys_AddAuditHook() can be called before Python is initialized. */
_PyRuntimeState *runtime = &_PyRuntime;
PyThreadState *tstate;
if (runtime->initialized) {
if (_PyRuntimeState_GetInitialized(runtime)) {
tstate = _PyThreadState_GET();
}
else {
Expand Down
Loading