From 669b34a404ea3af73da85d6f18af2a09ced02aa0 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 9 Aug 2026 12:57:46 +0300 Subject: [PATCH] gh-86427: Fix the stdio encoding in the legacy Windows stdio mode It was the ANSI code page instead of the encoding of the device the stream is connected to, as in 3.7. The stdio encoding is now left undefined in this mode and determined for every standard stream. --- Lib/test/test_cmd_line.py | 36 +++++++++++++++++++ ...09-05-30-00.gh-issue-86427.legacystdio.rst | 3 ++ Objects/unicodeobject.c | 4 +++ Python/initconfig.c | 16 ++++++--- Python/pylifecycle.c | 13 ++++++- 5 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Windows/2026-08-09-05-30-00.gh-issue-86427.legacystdio.rst diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index 25d6d1a248b457..caa8939f0cc211 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -1067,6 +1067,42 @@ def test_python_legacy_windows_stdio(self): support.skip_on_low_desktop_heap_memory_subprocess(p.returncode) self.assertEqual(p.returncode, 0) + @unittest.skipUnless(support.MS_WINDOWS, 'Test only applicable on Windows') + def test_python_legacy_windows_stdio_encoding(self): + # gh-86427: In the legacy mode the encoding of a standard stream is + # the encoding of the console it is connected to, which can differ + # for input and output. + import ctypes + kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) + try: + fin = open('CONIN$') + except OSError: + self.skipTest('no console') + # We cannot use PIPE, because the standard streams should be + # connected to the console. So we use the exit code. + code = ("import sys; sys.exit(sys.stdin.encoding != 'cp850' or " + "sys.stdout.encoding != 'cp437')") + env = os.environ.copy() + env['PYTHONLEGACYWINDOWSSTDIO'] = '1' + env['PYTHONUTF8'] = '0' + env.pop('PYTHONIOENCODING', None) + old_cp = kernel32.GetConsoleCP() + old_output_cp = kernel32.GetConsoleOutputCP() + with fin, open('CONOUT$', 'w') as fout: + try: + if not kernel32.SetConsoleCP(850): + self.skipTest('cannot set the console input code page') + if not kernel32.SetConsoleOutputCP(437): + self.skipTest('cannot set the console output code page') + proc = subprocess.run([sys.executable, '-c', code], env=env, + stdin=fin, stdout=fout, + stderr=subprocess.DEVNULL) + finally: + kernel32.SetConsoleCP(old_cp) + kernel32.SetConsoleOutputCP(old_output_cp) + support.skip_on_low_desktop_heap_memory_subprocess(proc.returncode) + self.assertEqual(proc.returncode, 0) + @unittest.skipIf("-fsanitize" in sysconfig.get_config_vars().get('PY_CFLAGS', ()), "PYTHONMALLOCSTATS doesn't work with ASAN") def test_python_malloc_stats(self): diff --git a/Misc/NEWS.d/next/Windows/2026-08-09-05-30-00.gh-issue-86427.legacystdio.rst b/Misc/NEWS.d/next/Windows/2026-08-09-05-30-00.gh-issue-86427.legacystdio.rst new file mode 100644 index 00000000000000..c3e2332defabfa --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-08-09-05-30-00.gh-issue-86427.legacystdio.rst @@ -0,0 +1,3 @@ +Fix the encoding of the standard streams in the legacy Windows stdio mode +(:envvar:`PYTHONLEGACYWINDOWSSTDIO`). It is now the encoding of the device +the stream is connected to, as in Python 3.7, not the ANSI code page. diff --git a/Objects/unicodeobject.c b/Objects/unicodeobject.c index 45d61c8b8b765a..f99079b18cce67 100644 --- a/Objects/unicodeobject.c +++ b/Objects/unicodeobject.c @@ -15209,6 +15209,10 @@ init_stdio_encoding(PyInterpreterState *interp) { /* Update the stdio encoding to the normalized Python codec name. */ PyConfig *config = (PyConfig*)_PyInterpreterState_GetConfig(interp); + if (config->stdio_encoding == NULL) { + /* gh-86427: The encoding is determined for every stream. */ + return _PyStatus_OK(); + } if (config_get_codec_name(&config->stdio_encoding) < 0) { return _PyStatus_ERR("failed to get the Python codec name " "of the stdio encoding"); diff --git a/Python/initconfig.c b/Python/initconfig.c index b9bacb17a66454..f3e881c01e329e 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -196,7 +196,7 @@ static const PyConfigSpec PYCONFIG_SPEC[] = { SPEC(show_ref_count, BOOL, READ_ONLY, NO_SYS, NO_GLOBAL), SPEC(site_import, BOOL, READ_ONLY, NO_SYS, GLOBAL(&Py_NoSiteFlag, 1)), // sys.flags.no_site SPEC(skip_source_first_line, BOOL, READ_ONLY, NO_SYS, NO_GLOBAL), - SPEC(stdio_encoding, WSTR, READ_ONLY, NO_SYS, NO_GLOBAL), + SPEC(stdio_encoding, WSTR_OPT, READ_ONLY, NO_SYS, NO_GLOBAL), SPEC(stdio_errors, WSTR, READ_ONLY, NO_SYS, NO_GLOBAL), SPEC(tracemalloc, UINT, READ_ONLY, NO_SYS, NO_GLOBAL), SPEC(use_frozen_modules, BOOL, READ_ONLY, NO_SYS, NO_GLOBAL), @@ -1074,11 +1074,14 @@ config_check_consistency(const PyConfig *config) assert(config->module_search_paths_set >= 0); assert(config->filesystem_encoding != NULL); assert(config->filesystem_errors != NULL); - assert(config->stdio_encoding != NULL); - assert(config->stdio_errors != NULL); #ifdef MS_WINDOWS + /* stdio_encoding can be NULL in the legacy Windows stdio mode. */ + assert(config->stdio_encoding != NULL || config->legacy_windows_stdio); assert(config->legacy_windows_stdio >= 0); +#else + assert(config->stdio_encoding != NULL); #endif + assert(config->stdio_errors != NULL); /* -c and -m options are exclusive */ assert(!(config->run_command != NULL && config->run_module != NULL)); assert(config->check_hash_pycs_mode != NULL); @@ -2709,7 +2712,12 @@ config_init_stdio_encoding(PyConfig *config, } /* Choose the default error handler based on the current locale. */ - if (config->stdio_encoding == NULL) { + if (config->stdio_encoding == NULL +#ifdef MS_WINDOWS + /* gh-86427: it is determined for each stream. */ + && !config->legacy_windows_stdio +#endif + ) { status = config_get_locale_encoding(config, preconfig, &config->stdio_encoding); if (_PyStatus_EXCEPTION(status)) { diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 500a1a1949a5a8..bc485fe0ba8558 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -3070,7 +3070,18 @@ create_stdio(const PyConfig *config, PyObject* io, newline = "\n"; #endif - PyObject *encoding_str = PyUnicode_FromWideChar(encoding, -1); + PyObject *encoding_str; + if (encoding != NULL) { + encoding_str = PyUnicode_FromWideChar(encoding, -1); + } + else { + /* gh-86427: use the encoding of the device. */ + encoding_str = _Py_device_encoding(fd); + if (encoding_str == Py_None) { + Py_DECREF(encoding_str); + encoding_str = _Py_GetLocaleEncodingObject(); + } + } if (encoding_str == NULL) { Py_CLEAR(buf); goto error;