From 2a92baf4cf3dffc487c8c5785bbc6695d383541c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 24 Sep 2026 14:43:12 +0200 Subject: [PATCH 1/4] gh-158080: Py_InitializeFromConfig() no longer exits Python 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. Renumber getopt.c long options to start at 1, instead of 0. --- Doc/c-api/init_config.rst | 34 ++-- Doc/c-api/interp-lifecycle.rst | 5 + Doc/whatsnew/3.16.rst | 8 + Include/cpython/initconfig.h | 15 +- Include/internal/pycore_initconfig.h | 2 + ...-09-24-15-27-35.gh-issue-158080.YIqypf.rst | 6 + Modules/main.c | 19 ++- Programs/_bootstrap_python.c | 3 - Python/getopt.c | 8 +- Python/initconfig.c | 152 ++++++++++++------ Python/pylifecycle.c | 7 +- 11 files changed, 171 insertions(+), 88 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2026-09-24-15-27-35.gh-issue-158080.YIqypf.rst diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index ef09639189a6c2..6fb09a8b01a25d 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``. - 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 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,10 @@ 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 processed in :c:func:`Py_RunMain`. .. _pyinitconfig-opts: @@ -690,9 +689,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 +754,8 @@ PyStatus Exit code. Argument passed to ``exit()``. + .. deprecated:: next + .. c:member:: const char *err_msg Error message. @@ -788,6 +786,11 @@ PyStatus Exit Python with the specified exit code. + .. deprecated:: next + :c:func:`Py_InitializeFromInitConfig` no longer sets an exit code if a + command line option wants to exit Python. Instead, the option is + processed in :c:func:`Py_RunMain`. + Functions to handle a status: .. c:function:: int PyStatus_Exception(PyStatus status) @@ -803,6 +806,11 @@ PyStatus Is the result an exit? + .. deprecated:: next + :c:func:`Py_InitializeFromInitConfig` no longer sets an exit code if a + command line option wants to exit Python. Instead, the option is + 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 6f832b7a34b10b..3a441725163a12 100644 --- a/Doc/c-api/interp-lifecycle.rst +++ b/Doc/c-api/interp-lifecycle.rst @@ -399,6 +399,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 c76681261f6746..8828a66a1cdb99 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 + processed in :c:func:`Py_RunMain`. + (Contributed by Victor Stinner in :gh:`158080`.) + .. Add C API deprecations above alphabetically, not here at the end. .. include:: ../deprecations/c-api-pending-removal-in-3.18.rst diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index 1ccc496c63ac78..cd4a4c765e9eaf 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 -1, 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 183b2d45c5ede1..50df2efdd4e526 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/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 00000000000000..5a5fc2317b67f4 --- /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 +processed in :c:func:`Py_RunMain`. Patch by Victor Stinner. diff --git a/Modules/main.c b/Modules/main.c index ef22331760906c..44a025d500e35e 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 d30ef8c879d815..c65d616afbd501 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/Python/getopt.c b/Python/getopt.c index 79bea2359ffffc..7e918189c716a9 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 464c76f9e3df2f..5b717e1097db19 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -217,6 +217,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = { SPEC(module_search_paths_set, BOOL, INIT_ONLY, NO_SYS, NO_GLOBAL), SPEC(pythonpath_env, WSTR_OPT, INIT_ONLY, NO_SYS, NO_GLOBAL), SPEC(sys_path_0, WSTR_OPT, INIT_ONLY, NO_SYS, NO_GLOBAL), + SPEC(_deferred_cmdline_option, INT, INIT_ONLY, NO_SYS, NO_GLOBAL), // Array terminator {NULL, 0, 0, 0, NO_SYS}, @@ -3085,13 +3086,17 @@ static PyStatus config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions, Py_ssize_t *opt_index) { + // DEFER_OPTION() only stores the first parsed deferred 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 { @@ -3134,7 +3139,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 @@ -3146,27 +3151,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++; @@ -3225,11 +3227,17 @@ 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++; + if (print_version >= 2) { + DEFER_OPTION('W'); + } + else { + DEFER_OPTION(c); + } break; case 'W': @@ -3251,16 +3259,10 @@ 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); - - if (print_version) { - printf("Python %s\n", - (print_version >= 2) ? Py_GetVersion() : PY_VERSION); - return _PyStatus_EXIT(0); - } + } while (config->_deferred_cmdline_option == 0); if (config->run_command == NULL && config->run_module == NULL && _PyOS_optind < argv->length @@ -3281,6 +3283,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 = -1; + 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; + } } @@ -4055,22 +4123,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; @@ -4086,13 +4139,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 a9c98d73fd0f72..8888f523559123 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 { From 674ce04c5d1f74ef5615b25c70d0231ceed90ec9 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 24 Sep 2026 16:26:44 +0200 Subject: [PATCH 2/4] Update test_embed --- Lib/test/test_embed.py | 1 + Programs/_testembed.c | 20 ++++++++------------ Python/initconfig.c | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 81241ea1f33733..748aeeafef0f45 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/Programs/_testembed.c b/Programs/_testembed.c index 7e82a6365d9808..286a602aea7603 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -1719,7 +1719,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) { @@ -1811,29 +1811,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/initconfig.c b/Python/initconfig.c index 5b717e1097db19..77a4f4ea45ec51 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -3292,7 +3292,7 @@ int _PyConfig_ProcessDeferredCmdlineOption(PyConfig *config) { int c = config->_deferred_cmdline_option; - config->_deferred_cmdline_option = -1; + config->_deferred_cmdline_option = 0; if (c == 0) { // There is no deferred option return -1; From f01c75010f2a26fc2d100277f814db2c2a63476f Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 25 Sep 2026 11:35:47 +0200 Subject: [PATCH 3/4] Fix python -VV * Make test_cmd_line.test_version() stricter * Adjust documentation. --- Doc/c-api/init_config.rst | 14 +++++++------- Doc/whatsnew/3.16.rst | 2 +- Lib/test/test_cmd_line.py | 15 ++++++++++----- ...2026-09-24-15-27-35.gh-issue-158080.YIqypf.rst | 2 +- Python/initconfig.c | 15 ++++++++------- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index 6fb09a8b01a25d..bfa5eda42b11d8 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -116,12 +116,12 @@ Error Handling Get the *config* exit code. - Return ``0``. + Return ``0`` and leave *\*exitcode* unchanged. 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 processed in :c:func:`Py_RunMain`. This - function became useless. + Python 3.16. Instead, the option is now processed in :c:func:`Py_RunMain`. + This function became useless. .. deprecated:: next @@ -787,9 +787,9 @@ PyStatus Exit Python with the specified exit code. .. deprecated:: next - :c:func:`Py_InitializeFromInitConfig` no longer sets an exit code if a + :c:func:`Py_InitializeFromConfig` no longer sets an exit code if a command line option wants to exit Python. Instead, the option is - processed in :c:func:`Py_RunMain`. + now processed in :c:func:`Py_RunMain`. Functions to handle a status: @@ -807,9 +807,9 @@ PyStatus Is the result an exit? .. deprecated:: next - :c:func:`Py_InitializeFromInitConfig` no longer sets an exit code if a + :c:func:`Py_InitializeFromConfig` no longer sets an exit code if a command line option wants to exit Python. Instead, the option is - processed in :c:func:`Py_RunMain`. + now processed in :c:func:`Py_RunMain`. .. c:function:: void Py_ExitStatusException(PyStatus status) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 2ed01e390f784d..7a1f9410c87fc7 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -1133,7 +1133,7 @@ Deprecated C APIs 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 - processed in :c:func:`Py_RunMain`. + now processed in :c:func:`Py_RunMain`. (Contributed by Victor Stinner in :gh:`158080`.) * :c:func:`PyModule_GetFilename` is no longer deprecated, but using diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index 4c1abb15c0cb14..476a481a0ba54b 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/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 index 5a5fc2317b67f4..4413b328d474e4 100644 --- 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 @@ -3,4 +3,4 @@ Deprecate :c:func:`PyInitConfig_GetExitCode`, :c:func:`PyStatus_Exit`, and 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 -processed in :c:func:`Py_RunMain`. Patch by Victor Stinner. +now processed in :c:func:`Py_RunMain`. Patch by Victor Stinner. diff --git a/Python/initconfig.c b/Python/initconfig.c index 98f75acc04e996..6de12db9d600ec 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -2997,7 +2997,7 @@ static PyStatus config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions, Py_ssize_t *opt_index) { - // DEFER_OPTION() only stores the first parsed deferred option + // Only store the first option #define DEFER_OPTION(OPTION) \ do { \ if (config->_deferred_cmdline_option == 0) { \ @@ -3143,12 +3143,6 @@ config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions, case 'V': print_version++; - if (print_version >= 2) { - DEFER_OPTION('W'); - } - else { - DEFER_OPTION(c); - } break; case 'W': @@ -3175,6 +3169,13 @@ config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions, } } while (config->_deferred_cmdline_option == 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 && _PyOS_optind < argv->length && wcscmp(argv->items[_PyOS_optind], L"-") != 0 From 665be155729c4d15cb772f3458dc3632395f7677 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 25 Sep 2026 12:59:48 +0200 Subject: [PATCH 4/4] Update doc/comments --- Doc/c-api/init_config.rst | 3 ++- Include/cpython/initconfig.h | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/c-api/init_config.rst b/Doc/c-api/init_config.rst index bfa5eda42b11d8..400a05758c9517 100644 --- a/Doc/c-api/init_config.rst +++ b/Doc/c-api/init_config.rst @@ -249,7 +249,8 @@ Initialize Python .. versionchanged:: next The function no longer sets an exit code if a command line option wants - to exit Python. Instead, the option is processed in :c:func:`Py_RunMain`. + to exit Python. Instead, the option is now processed in + :c:func:`Py_RunMain`. .. _pyinitconfig-opts: diff --git a/Include/cpython/initconfig.h b/Include/cpython/initconfig.h index cd4a4c765e9eaf..846e05fb26a4d8 100644 --- a/Include/cpython/initconfig.h +++ b/Include/cpython/initconfig.h @@ -245,7 +245,7 @@ typedef struct PyConfig { // 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 -1, there is no option. + // If equals to 0, there is no option. int _deferred_cmdline_option; } PyConfig;