diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index 25d6d1a248b4577..caa8939f0cc211f 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 000000000000000..c3e2332defabfa9 --- /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 45d61c8b8b765a6..f99079b18cce672 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 b9bacb17a66454a..f3e881c01e329ee 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 500a1a1949a5a8a..bc485fe0ba85587 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;