Skip to content

Commit 4e7a614

Browse files
gh-86427: Replace PyConfig.stdio_encoding with three separate members
PyConfig.stdin_encoding, PyConfig.stdout_encoding and PyConfig.stderr_encoding allow the standard streams to have different encodings. In the legacy Windows stdio mode they are initialized with the encoding of the device the corresponding stream is connected to, instead of the ANSI code page.
1 parent 0bb312a commit 4e7a614

13 files changed

Lines changed: 158 additions & 65 deletions

Doc/c-api/init_config.rst

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,12 @@ Configuration Options
477477
- :c:member:`skip_source_first_line <PyConfig.skip_source_first_line>`
478478
- ``bool``
479479
- Read-only
480-
* - ``"stdio_encoding"``
481-
- :c:member:`stdio_encoding <PyConfig.stdio_encoding>`
480+
* - ``"stderr_encoding"``
481+
- :c:member:`stderr_encoding <PyConfig.stderr_encoding>`
482+
* - ``"stdin_encoding"``
483+
- :c:member:`stdin_encoding <PyConfig.stdin_encoding>`
484+
* - ``"stdout_encoding"``
485+
- :c:member:`stdout_encoding <PyConfig.stdout_encoding>`
482486
- ``str``
483487
- Read-only
484488
* - ``"stdio_errors"``
@@ -1863,12 +1867,14 @@ PyConfig
18631867
18641868
Default: ``0``.
18651869
1866-
.. c:member:: wchar_t* stdio_encoding
1870+
.. c:member:: wchar_t* stdin_encoding
1871+
.. c:member:: wchar_t* stdout_encoding
1872+
.. c:member:: wchar_t* stderr_encoding
18671873
.. c:member:: wchar_t* stdio_errors
18681874
1869-
Encoding and encoding errors of :data:`sys.stdin`, :data:`sys.stdout` and
1870-
:data:`sys.stderr` (but :data:`sys.stderr` always uses
1871-
``"backslashreplace"`` error handler).
1875+
Encoding of :data:`sys.stdin`, :data:`sys.stdout` and :data:`sys.stderr`
1876+
respectively, and encoding errors of all of them (but :data:`sys.stderr`
1877+
always uses the ``"backslashreplace"`` error handler).
18721878
18731879
Use the :envvar:`PYTHONIOENCODING` environment variable if it is
18741880
non-empty.

Doc/library/sys.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2139,7 +2139,8 @@ always available. Unless explicitly noted otherwise, all variables are read-only
21392139
follows:
21402140

21412141
* The encoding and error handling are initialized from
2142-
:c:member:`PyConfig.stdio_encoding` and :c:member:`PyConfig.stdio_errors`.
2142+
:c:member:`PyConfig.stdin_encoding`, :c:member:`PyConfig.stdout_encoding`,
2143+
:c:member:`PyConfig.stderr_encoding` and :c:member:`PyConfig.stdio_errors`.
21432144

21442145
On Windows, UTF-8 is used for the console device. Non-character
21452146
devices such as disk files and pipes use the system locale

Include/cpython/initconfig.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,9 @@ typedef struct PyConfig {
171171
int user_site_directory;
172172
int configure_c_stdio;
173173
int buffered_stdio;
174-
wchar_t *stdio_encoding;
174+
wchar_t *stdin_encoding;
175+
wchar_t *stdout_encoding;
176+
wchar_t *stderr_encoding;
175177
wchar_t *stdio_errors;
176178
#ifdef MS_WINDOWS
177179
int legacy_windows_stdio;

Lib/test/test_capi/test_config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ def test_config_get(self):
8686
("show_ref_count", bool, None),
8787
("site_import", bool, None),
8888
("skip_source_first_line", bool, None),
89-
("stdio_encoding", str, None),
89+
("stderr_encoding", str, None),
90+
("stdin_encoding", str, None),
91+
("stdout_encoding", str, None),
9092
("stdio_errors", str, None),
9193
("stdlib_dir", str | None, "_stdlib_dir"),
9294
("tracemalloc", int, None),

Lib/test/test_cmd_line.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,8 +1069,9 @@ def test_python_legacy_windows_stdio(self):
10691069

10701070
@unittest.skipUnless(support.MS_WINDOWS, 'Test only applicable on Windows')
10711071
def test_python_legacy_windows_stdio_encoding(self):
1072-
# gh-86427: In the legacy mode the encoding of the standard streams
1073-
# is the encoding of the console.
1072+
# gh-86427: In the legacy mode the encoding of a standard stream is
1073+
# the encoding of the console it is connected to, which can differ
1074+
# for input and output.
10741075
import ctypes
10751076
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
10761077
try:
@@ -1080,7 +1081,7 @@ def test_python_legacy_windows_stdio_encoding(self):
10801081
# We cannot use PIPE, because the standard streams should be
10811082
# connected to the console. So we use the exit code.
10821083
code = ("import sys; sys.exit(sys.stdin.encoding != 'cp850' or "
1083-
"sys.stdout.encoding != 'cp850')")
1084+
"sys.stdout.encoding != 'cp437')")
10841085
env = os.environ.copy()
10851086
env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
10861087
env['PYTHONUTF8'] = '0'
@@ -1091,7 +1092,7 @@ def test_python_legacy_windows_stdio_encoding(self):
10911092
try:
10921093
if not kernel32.SetConsoleCP(850):
10931094
self.skipTest('cannot set the console input code page')
1094-
if not kernel32.SetConsoleOutputCP(850):
1095+
if not kernel32.SetConsoleOutputCP(437):
10951096
self.skipTest('cannot set the console output code page')
10961097
proc = subprocess.run([sys.executable, '-c', code], env=env,
10971098
stdin=fin, stdout=fout,

Lib/test/test_embed.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -767,7 +767,9 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
767767
'configure_c_stdio': False,
768768
'buffered_stdio': True,
769769

770-
'stdio_encoding': GET_DEFAULT_CONFIG,
770+
'stdin_encoding': GET_DEFAULT_CONFIG,
771+
'stdout_encoding': GET_DEFAULT_CONFIG,
772+
'stderr_encoding': GET_DEFAULT_CONFIG,
771773
'stdio_errors': GET_DEFAULT_CONFIG,
772774

773775
'skip_source_first_line': False,
@@ -934,16 +936,19 @@ def get_expected_config(self, expected_preconfig, expected,
934936
# there is no easy way to get the locale encoding before
935937
# setlocale(LC_CTYPE, "") is called: don't test encodings
936938
for key in ('filesystem_encoding', 'filesystem_errors',
937-
'stdio_encoding', 'stdio_errors'):
939+
'stdin_encoding', 'stdout_encoding',
940+
'stderr_encoding', 'stdio_errors'):
938941
expected[key] = self.IGNORE_CONFIG
939942

940943
if expected_preconfig['utf8_mode'] == 1:
941944
if expected['filesystem_encoding'] is self.GET_DEFAULT_CONFIG:
942945
expected['filesystem_encoding'] = 'utf-8'
943946
if expected['filesystem_errors'] is self.GET_DEFAULT_CONFIG:
944947
expected['filesystem_errors'] = self.UTF8_MODE_ERRORS
945-
if expected['stdio_encoding'] is self.GET_DEFAULT_CONFIG:
946-
expected['stdio_encoding'] = 'utf-8'
948+
for key in ('stdin_encoding', 'stdout_encoding',
949+
'stderr_encoding'):
950+
if expected[key] is self.GET_DEFAULT_CONFIG:
951+
expected[key] = 'utf-8'
947952
if expected['stdio_errors'] is self.GET_DEFAULT_CONFIG:
948953
expected['stdio_errors'] = 'surrogateescape'
949954

@@ -1133,7 +1138,9 @@ def test_init_from_config(self):
11331138
'malloc_stats': True,
11341139
'pymalloc_hugepages': True,
11351140

1136-
'stdio_encoding': 'iso8859-1',
1141+
'stdin_encoding': 'iso8859-1',
1142+
'stdout_encoding': 'iso8859-1',
1143+
'stderr_encoding': 'iso8859-1',
11371144
'stdio_errors': 'replace',
11381145

11391146
'pycache_prefix': 'conf_pycache_prefix',
@@ -1205,7 +1212,9 @@ def test_init_compat_env(self):
12051212
'write_bytecode': False,
12061213
'verbose': 1,
12071214
'buffered_stdio': False,
1208-
'stdio_encoding': 'iso8859-1',
1215+
'stdin_encoding': 'iso8859-1',
1216+
'stdout_encoding': 'iso8859-1',
1217+
'stderr_encoding': 'iso8859-1',
12091218
'stdio_errors': 'replace',
12101219
'user_site_directory': False,
12111220
'faulthandler': True,
@@ -1242,7 +1251,9 @@ def test_init_python_env(self):
12421251
'write_bytecode': False,
12431252
'verbose': 1,
12441253
'buffered_stdio': False,
1245-
'stdio_encoding': 'iso8859-1',
1254+
'stdin_encoding': 'iso8859-1',
1255+
'stdout_encoding': 'iso8859-1',
1256+
'stderr_encoding': 'iso8859-1',
12461257
'stdio_errors': 'replace',
12471258
'user_site_directory': False,
12481259
'faulthandler': True,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Replace :c:member:`!PyConfig.stdio_encoding` with three separate members:
2+
:c:member:`PyConfig.stdin_encoding`, :c:member:`PyConfig.stdout_encoding` and
3+
:c:member:`PyConfig.stderr_encoding`. In the legacy Windows stdio mode they
4+
are initialized with the encoding of the device the corresponding stream is
5+
connected to.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix the encoding of the standard streams in the legacy Windows stdio mode
2+
(:envvar:`PYTHONLEGACYWINDOWSSTDIO`). It is now the encoding of the device
3+
the stream is connected to, as in Python 3.7, not the ANSI code page.

Misc/NEWS.d/next/Windows/2026-08-09-07-00-00.gh-issue-86427.consolecp.rst

Lines changed: 0 additions & 3 deletions
This file was deleted.

Objects/unicodeobject.c

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15209,9 +15209,14 @@ init_stdio_encoding(PyInterpreterState *interp)
1520915209
{
1521015210
/* Update the stdio encoding to the normalized Python codec name. */
1521115211
PyConfig *config = (PyConfig*)_PyInterpreterState_GetConfig(interp);
15212-
if (config_get_codec_name(&config->stdio_encoding) < 0) {
15213-
return _PyStatus_ERR("failed to get the Python codec name "
15214-
"of the stdio encoding");
15212+
wchar_t **encodings[] = {&config->stdin_encoding,
15213+
&config->stdout_encoding,
15214+
&config->stderr_encoding};
15215+
for (size_t i = 0; i < Py_ARRAY_LENGTH(encodings); i++) {
15216+
if (config_get_codec_name(encodings[i]) < 0) {
15217+
return _PyStatus_ERR("failed to get the Python codec name "
15218+
"of the stdio encoding");
15219+
}
1521515220
}
1521615221
return _PyStatus_OK();
1521715222
}

0 commit comments

Comments
 (0)