diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py index 25d6d1a248b4577..7042c99c37d91ee 100644 --- a/Lib/test/test_cmd_line.py +++ b/Lib/test/test_cmd_line.py @@ -1067,6 +1067,41 @@ 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 the standard streams + # is the encoding of the console. + 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 != 'cp850')") + 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(850): + 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-07-00-00.gh-issue-86427.consolecp.rst b/Misc/NEWS.d/next/Windows/2026-08-09-07-00-00.gh-issue-86427.consolecp.rst new file mode 100644 index 000000000000000..1cbf188c12e4070 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-08-09-07-00-00.gh-issue-86427.consolecp.rst @@ -0,0 +1,3 @@ +Fix the encoding of the standard streams in the legacy Windows stdio mode +(:envvar:`PYTHONLEGACYWINDOWSSTDIO`). It is now the code page of the +console, as in Python 3.7, not the ANSI code page. diff --git a/Python/initconfig.c b/Python/initconfig.c index b9bacb17a66454a..69d47a5872f5e38 100644 --- a/Python/initconfig.c +++ b/Python/initconfig.c @@ -2710,8 +2710,30 @@ config_init_stdio_encoding(PyConfig *config, /* Choose the default error handler based on the current locale. */ if (config->stdio_encoding == NULL) { - status = config_get_locale_encoding(config, preconfig, - &config->stdio_encoding); +#ifdef MS_WINDOWS + /* gh-86427: use the console code page. Only one encoding can be + specified, so the output code page is used: it affects two + streams of three. */ + UINT cp = config->legacy_windows_stdio ? GetConsoleOutputCP() : 0; + if (cp != 0) { + if (cp == CP_UTF8) { + status = PyConfig_SetString(config, &config->stdio_encoding, + L"utf-8"); + } + else { + wchar_t encoding[20]; + swprintf(encoding, Py_ARRAY_LENGTH(encoding), L"cp%u", + (unsigned int)cp); + status = PyConfig_SetString(config, &config->stdio_encoding, + encoding); + } + } + else +#endif + { + status = config_get_locale_encoding(config, preconfig, + &config->stdio_encoding); + } if (_PyStatus_EXCEPTION(status)) { return status; }