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]

Reply via email to