https://github.com/python/cpython/commit/d9b739040c9083cf9bdc751c2268ece1420b336d
commit: d9b739040c9083cf9bdc751c2268ece1420b336d
branch: 3.13
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-29T18:42:14Z
summary:
[3.13] gh-87587: Fix os.device_encoding() for any console on Windows
(GH-155410) (GH-156597)
It was hard coded to map file descriptors 0, 1 and 2 to the console code
page, but a console can be opened as any file descriptor, and other
character devices, like NUL, are not consoles.
The file handle is now queried. The UTF-8 code page is also now reported
as "utf-8" instead of "cp65001".
(cherry picked from commit b4aea41d1cd0da1f34e42366d64e9e19d557eb87)
files:
A Misc/NEWS.d/next/Windows/2026-08-09-04-00-00.gh-issue-87587.devenc.rst
M Lib/test/test_os.py
M Python/fileutils.c
diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index b50590c7a289599..5836cb7a3181f18 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -3408,6 +3408,45 @@ def test_device_encoding(self):
self.assertTrue(codecs.lookup(encoding))
[email protected](sys.platform == "win32", "Win32 specific tests")
+class Win32DeviceEncodingTests(unittest.TestCase):
+ # gh-87587: any console file descriptor is supported, not only 0, 1 and 2,
+ # and other character devices are not consoles.
+
+ @staticmethod
+ def expected_encoding(cp):
+ return 'utf-8' if cp == 65001 else 'cp%d' % cp
+
+ def test_console(self):
+ import ctypes
+ kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
+ try:
+ fin = open('CONIN$')
+ except OSError:
+ self.skipTest('no console')
+ with fin:
+ self.assertEqual(os.device_encoding(fin.fileno()),
+ self.expected_encoding(kernel32.GetConsoleCP()))
+ with open('CONOUT$', 'w') as fout:
+ self.assertEqual(
+ os.device_encoding(fout.fileno()),
+ self.expected_encoding(kernel32.GetConsoleOutputCP()))
+
+ def test_not_a_console(self):
+ with open('NUL', 'w') as f:
+ self.assertTrue(os.isatty(f.fileno()))
+ self.assertIsNone(os.device_encoding(f.fileno()))
+ # Not a console even if it is a standard file descriptor.
+ saved = os.dup(1)
+ try:
+ os.dup2(f.fileno(), 1)
+ encoding = os.device_encoding(1)
+ finally:
+ os.dup2(saved, 1)
+ os.close(saved)
+ self.assertIsNone(encoding)
+
+
@support.requires_subprocess()
class PidTests(unittest.TestCase):
@unittest.skipUnless(hasattr(os, 'getppid'), "test needs os.getppid")
diff --git
a/Misc/NEWS.d/next/Windows/2026-08-09-04-00-00.gh-issue-87587.devenc.rst
b/Misc/NEWS.d/next/Windows/2026-08-09-04-00-00.gh-issue-87587.devenc.rst
new file mode 100644
index 000000000000000..06bba3e1f3d8558
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2026-08-09-04-00-00.gh-issue-87587.devenc.rst
@@ -0,0 +1,4 @@
+:func:`os.device_encoding` on Windows now returns the code page of any
+console file descriptor, not only 0, 1 and 2, and returns ``None`` for other
+character devices like ``NUL``. The UTF-8 code page is now reported as
+``"utf-8"`` instead of ``"cp65001"``.
diff --git a/Python/fileutils.c b/Python/fileutils.c
index 921500bfef1d4e4..25fb92ace88f800 100644
--- a/Python/fileutils.c
+++ b/Python/fileutils.c
@@ -77,34 +77,59 @@ get_surrogateescape(_Py_error_handler errors, int
*surrogateescape)
PyObject *
_Py_device_encoding(int fd)
{
- int valid;
- Py_BEGIN_ALLOW_THREADS
+#if defined(MS_WINDOWS) && defined(HAVE_WINDOWS_CONSOLE_IO)
+ HANDLE handle;
+ DWORD temp;
+ UINT cp = 0;
+
_Py_BEGIN_SUPPRESS_IPH
- valid = isatty(fd);
+ handle = (HANDLE)_get_osfhandle(fd);
_Py_END_SUPPRESS_IPH
- Py_END_ALLOW_THREADS
- if (!valid)
+ if (handle == INVALID_HANDLE_VALUE) {
Py_RETURN_NONE;
+ }
+
+ Py_BEGIN_ALLOW_THREADS
+ if (GetFileType(handle) == FILE_TYPE_CHAR) {
+ /* GetConsoleMode() only succeeds for a console handle. */
+ if (!GetConsoleMode(handle, &temp)) {
+ /* Assume that access denied implies an output handle. */
+ if (GetLastError() == ERROR_ACCESS_DENIED) {
+ cp = GetConsoleOutputCP();
+ }
+ }
+ else if (GetNumberOfConsoleInputEvents(handle, &temp)) {
+ cp = GetConsoleCP();
+ }
+ else {
+ cp = GetConsoleOutputCP();
+ }
+ }
+ Py_END_ALLOW_THREADS
-#ifdef MS_WINDOWS
-#ifdef HAVE_WINDOWS_CONSOLE_IO
- UINT cp;
- if (fd == 0)
- cp = GetConsoleCP();
- else if (fd == 1 || fd == 2)
- cp = GetConsoleOutputCP();
- else
- cp = 0;
/* GetConsoleCP() and GetConsoleOutputCP() return 0 if the application
has no console */
+ if (cp == CP_UTF8) {
+ _Py_DECLARE_STR(utf_8, "utf-8");
+ return &_Py_STR(utf_8);
+ }
if (cp == 0) {
Py_RETURN_NONE;
}
-
return PyUnicode_FromFormat("cp%u", (unsigned int)cp);
#else
+ int valid;
+ Py_BEGIN_ALLOW_THREADS
+ _Py_BEGIN_SUPPRESS_IPH
+ valid = isatty(fd);
+ _Py_END_SUPPRESS_IPH
+ Py_END_ALLOW_THREADS
+ if (!valid) {
+ Py_RETURN_NONE;
+ }
+
+#ifdef MS_WINDOWS
Py_RETURN_NONE;
-#endif /* HAVE_WINDOWS_CONSOLE_IO */
#else
if (_PyRuntime.preconfig.utf8_mode) {
_Py_DECLARE_STR(utf_8, "utf-8");
@@ -112,6 +137,7 @@ _Py_device_encoding(int fd)
}
return _Py_GetLocaleEncodingObject();
#endif
+#endif /* MS_WINDOWS && HAVE_WINDOWS_CONSOLE_IO */
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]