https://github.com/python/cpython/commit/fe7717c6116402a05854e0d6ccb1016990f378b0
commit: fe7717c6116402a05854e0d6ccb1016990f378b0
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-09-25T11:34:05Z
summary:
gh-158080: Py_InitializeFromConfig() no longer exits Python (#158085)
If a command line option wants to exit Python, move this option
processing from PyConfig_Read() to Py_Main(). So
Py_InitializeFromInitConfig() and Py_InitializeFromConfig() can no
longer return an exit code.
Deprecate PyInitConfig_GetExitCode(), PyStatus_Exit(), and PyStatus_IsExit()
functions, and PyStatus.exitcode member.
Renumber getopt.c long options to start at 1, instead of 0.
files:
A Misc/NEWS.d/next/C_API/2026-09-24-15-27-35.gh-issue-158080.YIqypf.rst
M Doc/c-api/init_config.rst
M Doc/c-api/interp-lifecycle.rst
M Doc/whatsnew/3.16.rst
M Include/cpython/initconfig.h
M Include/internal/pycore_initconfig.h
M Lib/test/test_cmd_line.py
M Lib/test/test_embed.py
M Modules/main.c
M Programs/_bootstrap_python.c
M Programs/_testembed.c
M Python/getopt.c
M Python/initconfig.c
M Python/pylifecycle.c
diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst
index ef09639189a6c27..400a05758c95174 100644
--- a/Doc/c-api/init_config.rst
+++ b/Doc/c-api/init_config.rst
@@ -116,15 +116,14 @@ Error Handling
Get the *config* exit code.
- * Set *\*exitcode* and return ``1`` if *config* has an exit code set.
- * Return ``0`` if *config* has no exit code set.
+ Return ``0`` and leave *\*exitcode* unchanged.
- Only the ``Py_InitializeFromInitConfig()`` function can set an exit
- code if the ``parse_argv`` option is non-zero.
+ In Python 3.15, :c:func:`Py_InitializeFromInitConfig` sets an exit code if a
+ command line option wants to exit Python. This is no longer the case in
+ Python 3.16. Instead, the option is now processed in :c:func:`Py_RunMain`.
+ This function became useless.
- An exit code can be set when parsing the command line failed (exit
- code ``2``) or when a command line option asks to display the command
- line help (exit code ``0``).
+ .. deprecated:: next
Get Options
@@ -247,10 +246,11 @@ Initialize Python
* Return ``0`` on success.
* Set an error in *config* and return ``-1`` on error.
- * Set an exit code in *config* and return ``-1`` if Python wants to
- exit.
- See ``PyInitConfig_GetExitcode()`` for the exit code case.
+ .. versionchanged:: next
+ The function no longer sets an exit code if a command line option wants
+ to exit Python. Instead, the option is now processed in
+ :c:func:`Py_RunMain`.
.. _pyinitconfig-opts:
@@ -690,9 +690,6 @@ Example of customized Python always running in isolated
mode::
exception:
PyConfig_Clear(&config);
- if (PyStatus_IsExit(status)) {
- return status.exitcode;
- }
/* Display the error message and exit the process with
non-zero exit code */
Py_ExitStatusException(status);
@@ -758,6 +755,8 @@ PyStatus
Exit code. Argument passed to ``exit()``.
+ .. deprecated:: next
+
.. c:member:: const char *err_msg
Error message.
@@ -788,6 +787,11 @@ PyStatus
Exit Python with the specified exit code.
+ .. deprecated:: next
+ :c:func:`Py_InitializeFromConfig` no longer sets an exit code if a
+ command line option wants to exit Python. Instead, the option is
+ now processed in :c:func:`Py_RunMain`.
+
Functions to handle a status:
.. c:function:: int PyStatus_Exception(PyStatus status)
@@ -803,6 +807,11 @@ PyStatus
Is the result an exit?
+ .. deprecated:: next
+ :c:func:`Py_InitializeFromConfig` no longer sets an exit code if a
+ command line option wants to exit Python. Instead, the option is
+ now processed in :c:func:`Py_RunMain`.
+
.. c:function:: void Py_ExitStatusException(PyStatus status)
Call ``exit(exitcode)`` if *status* is an exit. Print the error
diff --git a/Doc/c-api/interp-lifecycle.rst b/Doc/c-api/interp-lifecycle.rst
index bd4125cacdd0e66..efd208263bdf339 100644
--- a/Doc/c-api/interp-lifecycle.rst
+++ b/Doc/c-api/interp-lifecycle.rst
@@ -127,6 +127,11 @@ Initializing and finalizing the interpreter
interpreter, populating the runtime configuration structure, and querying
the returned status structure.
+ .. versionchanged:: next
+ The function no longer returns an exit code if a command line option
+ wants to exit Python. Instead, the option is processed in
+ :c:func:`Py_RunMain`.
+
.. c:function:: int Py_IsInitialized()
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index 3d3125ad17b126b..7a1f9410c87fc7d 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -1128,6 +1128,14 @@ Deprecated C APIs
and :c:func:`PyAsyncGen_New` are deprecated.
They are scheduled for removal in 3.18.
+* Deprecate :c:func:`PyInitConfig_GetExitCode`, :c:func:`PyStatus_Exit`,
+ and :c:func:`PyStatus_IsExit` functions, and :c:member:`PyStatus.exitcode`
+ member. :c:func:`Py_InitializeFromInitConfig` and
+ :c:func:`Py_InitializeFromConfig` can no longer return an exit code.
+ Instead, if a command line option wants to exit Python, the option is
+ now processed in :c:func:`Py_RunMain`.
+ (Contributed by Victor Stinner in :gh:`158080`.)
+
* :c:func:`PyModule_GetFilename` is no longer deprecated, but using
:c:func:`PyModule_GetFilenameObject` instead is still recommended.
(Contributed by Victor Stinner in :gh:`154757`.)
diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h
index 1ccc496c63ac780..846e05fb26a4d83 100644
--- a/Include/cpython/initconfig.h
+++ b/Include/cpython/initconfig.h
@@ -11,19 +11,19 @@ typedef struct {
enum {
_PyStatus_TYPE_OK=0,
_PyStatus_TYPE_ERROR=1,
- _PyStatus_TYPE_EXIT=2
+ _PyStatus_TYPE_EXIT=2 // deprecated
} _type;
const char *func;
const char *err_msg;
- int exitcode;
+ Py_DEPRECATED(3.16) int exitcode;
} PyStatus;
PyAPI_FUNC(PyStatus) PyStatus_Ok(void);
PyAPI_FUNC(PyStatus) PyStatus_Error(const char *err_msg);
PyAPI_FUNC(PyStatus) PyStatus_NoMemory(void);
-PyAPI_FUNC(PyStatus) PyStatus_Exit(int exitcode);
+Py_DEPRECATED(3.16) PyAPI_FUNC(PyStatus) PyStatus_Exit(int exitcode);
PyAPI_FUNC(int) PyStatus_IsError(PyStatus err);
-PyAPI_FUNC(int) PyStatus_IsExit(PyStatus err);
+Py_DEPRECATED(3.16) PyAPI_FUNC(int) PyStatus_IsExit(PyStatus err);
PyAPI_FUNC(int) PyStatus_Exception(PyStatus err);
/* --- PyWideStringList ------------------------------------------------ */
@@ -242,6 +242,11 @@ typedef struct PyConfig {
// PYTHON_PRESITE=package.module or -X presite=package.module
wchar_t *run_presite;
#endif
+
+ // If a command line option wants to exit Python, store it in this member
+ // and only process the option in Py_RunMain() instead of PyConfig_Read().
+ // If equals to 0, there is no option.
+ int _deferred_cmdline_option;
} PyConfig;
PyAPI_FUNC(void) PyConfig_InitPythonConfig(PyConfig *config);
@@ -293,7 +298,7 @@ PyAPI_FUNC(void) PyInitConfig_Free(PyInitConfig *config);
PyAPI_FUNC(int) PyInitConfig_GetError(PyInitConfig* config,
const char **err_msg);
-PyAPI_FUNC(int) PyInitConfig_GetExitCode(PyInitConfig* config,
+Py_DEPRECATED(3.16) PyAPI_FUNC(int) PyInitConfig_GetExitCode(PyInitConfig*
config,
int *exitcode);
PyAPI_FUNC(int) PyInitConfig_HasOption(PyInitConfig *config,
diff --git a/Include/internal/pycore_initconfig.h
b/Include/internal/pycore_initconfig.h
index 183b2d45c5ede1c..50df2efdd4e5269 100644
--- a/Include/internal/pycore_initconfig.h
+++ b/Include/internal/pycore_initconfig.h
@@ -182,6 +182,8 @@ extern PyObject* _PyConfig_CreateXOptionsDict(const
PyConfig *config);
extern void _Py_DumpPathConfig(PyThreadState *tstate);
+extern int _PyConfig_ProcessDeferredCmdlineOption(PyConfig *config);
+
/* --- Function used for testing ---------------------------------- */
diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py
index 4c1abb15c0cb148..476a481a0ba54b3 100644
--- a/Lib/test/test_cmd_line.py
+++ b/Lib/test/test_cmd_line.py
@@ -133,11 +133,16 @@ def test_site_flag(self):
@support.cpython_only
def test_version(self):
- version = ('Python %d.%d' % sys.version_info[:2]).encode("ascii")
- for switch in '-V', '--version', '-VV':
- rc, out, err = assert_python_ok(switch)
- self.assertNotStartsWith(err, version)
- self.assertStartsWith(out, version)
+ short_version = ('Python %d.%d' % sys.version_info[:2])
+ for switch in ('-V', '--version'):
+ with self.subTest(switch=switch):
+ rc, out, err = assert_python_ok(switch)
+ self.assertStartsWith(out, short_version.encode())
+ self.assertEqual(err, b'')
+
+ rc, out, err = assert_python_ok('-VV')
+ self.assertEqual(out.rstrip(), f"Python {sys.version}".encode())
+ self.assertEqual(err, b'')
def test_verbose(self):
# -v causes imports to write to stderr. If the write to
diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py
index 77b231323cc2757..39485afcdb0f57d 100644
--- a/Lib/test/test_embed.py
+++ b/Lib/test/test_embed.py
@@ -783,6 +783,7 @@ class InitConfigTests(EmbeddingTestsMixin,
unittest.TestCase):
'use_frozen_modules': not support.Py_DEBUG,
'safe_path': False,
'_is_python_build': IGNORE_CONFIG,
+ '_deferred_cmdline_option': 0,
}
if Py_STATS:
CONFIG_COMPAT['_pystats'] = False
diff --git
a/Misc/NEWS.d/next/C_API/2026-09-24-15-27-35.gh-issue-158080.YIqypf.rst
b/Misc/NEWS.d/next/C_API/2026-09-24-15-27-35.gh-issue-158080.YIqypf.rst
new file mode 100644
index 000000000000000..4413b328d474e48
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2026-09-24-15-27-35.gh-issue-158080.YIqypf.rst
@@ -0,0 +1,6 @@
+Deprecate :c:func:`PyInitConfig_GetExitCode`, :c:func:`PyStatus_Exit`, and
+:c:func:`PyStatus_IsExit` functions, and :c:member:`PyStatus.exitcode`
+member. :c:func:`Py_InitializeFromInitConfig` and
+:c:func:`Py_InitializeFromConfig` can no longer return an exit code.
+Instead, if a command line option wants to exit Python, the option is
+now processed in :c:func:`Py_RunMain`. Patch by Victor Stinner.
diff --git a/Modules/main.c b/Modules/main.c
index ef22331760906ce..44a025d500e35e2 100644
--- a/Modules/main.c
+++ b/Modules/main.c
@@ -749,13 +749,21 @@ pymain_set_path0(PyObject *main_importer_path)
static void
pymain_run_python(int *exitcode)
{
- int set_running_main = 0;
-
- PyObject *main_importer_path = NULL;
PyInterpreterState *interp = _PyInterpreterState_GET();
/* pymain_repl() and pymain_run_stdin() modify the config */
PyConfig *config = (PyConfig*)_PyInterpreterState_GetConfig(interp);
+ // Process command line options which want to exit Python
+ int cmdline_exitcode = _PyConfig_ProcessDeferredCmdlineOption(config);
+ if (cmdline_exitcode >= 0) {
+ *exitcode = cmdline_exitcode;
+ return;
+ }
+
+ int set_running_main = 0;
+
+ PyObject *main_importer_path = NULL;
+
/* ensure path config is written into global variables */
PyStatus status = _PyPathConfig_UpdateGlobal(config);
if (_PyStatus_EXCEPTION(status)) {
@@ -910,10 +918,7 @@ static int
pymain_main(_PyArgv *args)
{
PyStatus status = pymain_init(args);
- if (_PyStatus_IS_EXIT(status)) {
- pymain_free();
- return status.exitcode;
- }
+ assert(!_PyStatus_IS_EXIT(status));
if (_PyStatus_EXCEPTION(status)) {
pymain_exit_error(status);
}
diff --git a/Programs/_bootstrap_python.c b/Programs/_bootstrap_python.c
index d30ef8c879d8153..c65d616afbd5010 100644
--- a/Programs/_bootstrap_python.c
+++ b/Programs/_bootstrap_python.c
@@ -104,9 +104,6 @@ main(int argc, char **argv)
error:
PyConfig_Clear(&config);
- if (PyStatus_IsExit(status)) {
- return status.exitcode;
- }
Py_ExitStatusException(status);
}
diff --git a/Programs/_testembed.c b/Programs/_testembed.c
index 79e817829c15947..592c23756201f01 100644
--- a/Programs/_testembed.c
+++ b/Programs/_testembed.c
@@ -1722,7 +1722,7 @@ static int test_initconfig_api(void)
goto error;
}
- // Set a list of UTF-8 strings (argv)
+ // Set a list of UTF-8 strings (xoptions)
char* xoptions[] = {"faulthandler"};
if (PyInitConfig_SetStrList(config, "xoptions",
Py_ARRAY_LENGTH(xoptions), xoptions) < 0) {
@@ -1814,29 +1814,25 @@ static int test_initconfig_get_api(void)
static int test_initconfig_exit(void)
{
+ // -h command line option is stored as PyConfig._deferred_cmdline_option
PyInitConfig *config = PyInitConfig_Create();
if (config == NULL) {
printf("Init allocation error\n");
return 1;
}
- char *argv[] = {PROGRAM_NAME_UTF8, "--help"};
+ char *argv[] = {PROGRAM_NAME_UTF8, "-h"};
assert(PyInitConfig_SetStrList(config, "argv",
Py_ARRAY_LENGTH(argv), argv) == 0);
-
assert(PyInitConfig_SetInt(config, "parse_argv", 1) == 0);
- assert(Py_InitializeFromInitConfig(config) < 0);
-
- int exitcode;
- assert(PyInitConfig_GetExitCode(config, &exitcode) == 1);
- assert(exitcode == 0);
+ assert(Py_InitializeFromInitConfig(config) == 0);
+ PyInitConfig_Free(config);
- const char *err_msg;
- assert(PyInitConfig_GetError(config, &err_msg) == 1);
- assert(strcmp(err_msg, "exit code 0") == 0);
+ const PyConfig *rt_config = _Py_GetConfig();
+ assert(rt_config->_deferred_cmdline_option == 'h');
- PyInitConfig_Free(config);
+ Py_Finalize();
return 0;
}
diff --git a/Python/getopt.c b/Python/getopt.c
index 79bea2359ffffcf..7e918189c716a9e 100644
--- a/Python/getopt.c
+++ b/Python/getopt.c
@@ -41,10 +41,10 @@ static const wchar_t *opt_ptr = L"";
static const _PyOS_LongOption longopts[] = {
/* name, has_arg, val (used in switch in initconfig.c) */
- {L"check-hash-based-pycs", 1, 0},
- {L"help-all", 0, 1},
- {L"help-env", 0, 2},
- {L"help-xoptions", 0, 3},
+ {L"check-hash-based-pycs", 1, 1},
+ {L"help-all", 0, 2},
+ {L"help-env", 0, 3},
+ {L"help-xoptions", 0, 4},
{NULL, 0, -1}, /* sentinel */
};
diff --git a/Python/initconfig.c b/Python/initconfig.c
index d683fdd6abc6e1b..6de12db9d600ecc 100644
--- a/Python/initconfig.c
+++ b/Python/initconfig.c
@@ -204,6 +204,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = {
SPEC(module_search_paths_set, BOOL, INIT_ONLY, NO_SYS),
SPEC(pythonpath_env, WSTR_OPT, INIT_ONLY, NO_SYS),
SPEC(sys_path_0, WSTR_OPT, INIT_ONLY, NO_SYS),
+ SPEC(_deferred_cmdline_option, INT, INIT_ONLY, NO_SYS),
// Array terminator
{NULL, 0, 0, 0, NO_SYS},
@@ -2996,13 +2997,17 @@ static PyStatus
config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions,
Py_ssize_t *opt_index)
{
+ // Only store the first option
+#define DEFER_OPTION(OPTION) \
+ do { \
+ if (config->_deferred_cmdline_option == 0) { \
+ config->_deferred_cmdline_option = (OPTION); \
+ } \
+ } while (0)
+
PyStatus status;
const PyWideStringList *argv = &config->argv;
int print_version = 0;
- const wchar_t* program = config->program_name;
- if (!program && argv->length >= 1) {
- program = argv->items[0];
- }
_PyOS_ResetGetOpt();
do {
@@ -3045,7 +3050,7 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
switch (c) {
// Integers represent long options, see Python/getopt.c
- case 0:
+ case 1:
// check-hash-based-pycs
if (wcscmp(_PyOS_optarg, L"always") == 0
|| wcscmp(_PyOS_optarg, L"never") == 0
@@ -3057,27 +3062,24 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
return status;
}
} else {
- fprintf(stderr, "--check-hash-based-pycs must be one of "
- "'default', 'always', or 'never'\n");
- config_usage(1, program);
- return _PyStatus_EXIT(2);
+ DEFER_OPTION(c);
}
break;
- case 1:
+ case 2:
// help-all
- config_complete_usage(program);
- return _PyStatus_EXIT(0);
+ DEFER_OPTION(c);
+ break;
- case 2:
+ case 3:
// help-env
- config_envvars_usage();
- return _PyStatus_EXIT(0);
+ DEFER_OPTION(c);
+ break;
- case 3:
+ case 4:
// help-xoptions
- config_xoptions_usage();
- return _PyStatus_EXIT(0);
+ DEFER_OPTION(c);
+ break;
case 'b':
config->bytes_warning++;
@@ -3136,8 +3138,8 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
case 'h':
case '?':
- config_usage(0, program);
- return _PyStatus_EXIT(0);
+ DEFER_OPTION(c);
+ break;
case 'V':
print_version++;
@@ -3162,15 +3164,16 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
default:
/* unknown argument: parsing failed */
- config_usage(1, program);
- return _PyStatus_EXIT(2);
+ DEFER_OPTION(c);
+ break;
}
- } while (1);
+ } while (config->_deferred_cmdline_option == 0);
- if (print_version) {
- printf("Python %s\n",
- (print_version >= 2) ? Py_GetVersion() : PY_VERSION);
- return _PyStatus_EXIT(0);
+ if (print_version >= 2) {
+ DEFER_OPTION('W');
+ }
+ else if (print_version >= 1) {
+ DEFER_OPTION('V');
}
if (config->run_command == NULL && config->run_module == NULL
@@ -3192,6 +3195,72 @@ config_parse_cmdline(PyConfig *config, PyWideStringList
*warnoptions,
*opt_index = _PyOS_optind;
return _PyStatus_OK();
+
+#undef DEFER_OPTION
+}
+
+
+int
+_PyConfig_ProcessDeferredCmdlineOption(PyConfig *config)
+{
+ int c = config->_deferred_cmdline_option;
+ config->_deferred_cmdline_option = 0;
+ if (c == 0) {
+ // There is no deferred option
+ return -1;
+ }
+
+ // Select the program name
+ const PyWideStringList *argv = &config->argv;
+ const wchar_t* program = config->program_name;
+ if (!program && argv->length >= 1) {
+ program = argv->items[0];
+ }
+ if (!program) {
+ program = L"python";
+ }
+
+ switch (c) {
+ case 'h':
+ case '?':
+ config_usage(0, program);
+ return 0;
+
+ case 'V':
+ printf("Python %s\n", PY_VERSION);
+ return 0;
+
+ case 'W': // -VV or more -V options
+ printf("Python %s\n", Py_GetVersion());
+ return 0;
+
+ // Integers represent long options, see Python/getopt.c
+ case 1:
+ // check-hash-based-pycs
+ fprintf(stderr, "--check-hash-based-pycs must be one of "
+ "'default', 'always', or 'never'\n");
+ config_usage(1, program);
+ return 2;
+
+ case 2:
+ // help-all
+ config_complete_usage(program);
+ return 0;
+
+ case 3:
+ // help-env
+ config_envvars_usage();
+ return 0;
+
+ case 4:
+ // help-xoptions
+ config_xoptions_usage();
+ return 0;
+
+ default:
+ config_usage(1, program);
+ return 2;
+ }
}
@@ -3634,7 +3703,6 @@ config_argv0_add_exe(PyConfig *config)
* Command line arguments
* Environment variables
- * Py_xxx global configuration variables
The only side effects are to modify config and to call _Py_SetArgcArgv(). */
PyStatus
@@ -3966,22 +4034,7 @@ PyInitConfig_Free(PyInitConfig *config)
int
PyInitConfig_GetError(PyInitConfig* config, const char **perr_msg)
{
- if (_PyStatus_IS_EXIT(config->status)) {
- char buffer[22]; // len("exit code -2147483648\0")
- PyOS_snprintf(buffer, sizeof(buffer),
- "exit code %i",
- config->status.exitcode);
-
- if (config->err_msg != NULL) {
- free(config->err_msg);
- }
- config->err_msg = strdup(buffer);
- if (config->err_msg != NULL) {
- *perr_msg = config->err_msg;
- return 1;
- }
- config->status = _PyStatus_NO_MEMORY();
- }
+ assert(!_PyStatus_IS_EXIT(config->status));
if (_PyStatus_IS_ERROR(config->status) && config->status.err_msg != NULL) {
*perr_msg = config->status.err_msg;
@@ -3997,13 +4050,8 @@ PyInitConfig_GetError(PyInitConfig* config, const char
**perr_msg)
int
PyInitConfig_GetExitCode(PyInitConfig* config, int *exitcode)
{
- if (_PyStatus_IS_EXIT(config->status)) {
- *exitcode = config->status.exitcode;
- return 1;
- }
- else {
- return 0;
- }
+ assert(!_PyStatus_IS_EXIT(config->status));
+ return 0;
}
diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c
index a9c98d73fd0f721..8888f523559123a 100644
--- a/Python/pylifecycle.c
+++ b/Python/pylifecycle.c
@@ -3830,10 +3830,9 @@ _Py_FatalRefcountErrorFunc(const char *func, const char
*msg)
void _Py_NO_RETURN
Py_ExitStatusException(PyStatus status)
{
- if (_PyStatus_IS_EXIT(status)) {
- exit(status.exitcode);
- }
- else if (_PyStatus_IS_ERROR(status)) {
+ assert(!_PyStatus_IS_EXIT(status));
+
+ if (_PyStatus_IS_ERROR(status)) {
fatal_error(fileno(stderr), 1, status.func, status.err_msg, 1);
}
else {
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]