https://github.com/python/cpython/commit/0ca0107e4fc842b57ec12a39a51c9ce390d43c4b
commit: 0ca0107e4fc842b57ec12a39a51c9ce390d43c4b
branch: main
author: Jason G <[email protected]>
committer: zooba <[email protected]>
date: 2026-07-23T15:52:42Z
summary:
gh-69573: Check the return value of msvcrt._putch() and _putwch() (GH-154364)
files:
A Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst
M Doc/library/msvcrt.rst
M Lib/test/test_msvcrt.py
M PC/msvcrtmodule.c
diff --git a/Doc/library/msvcrt.rst b/Doc/library/msvcrt.rst
index 6b49c1a9ccd6e11..f4a1125554a0513 100644
--- a/Doc/library/msvcrt.rst
+++ b/Doc/library/msvcrt.rst
@@ -135,13 +135,23 @@ Console I/O
.. function:: putch(char)
- Print the byte string *char* to the console without buffering.
+ Print the byte string *char* to the console without buffering. Raises
+ :exc:`OSError` on failure, for example when the process has no console
+ attached.
+
+ .. versionchanged:: next
+ Failures are now reported by raising :exc:`OSError` instead of being
+ silently ignored.
.. function:: putwch(unicode_char)
Wide char variant of :func:`putch`, accepting a Unicode value.
+ .. versionchanged:: next
+ Failures are now reported by raising :exc:`OSError` instead of being
+ silently ignored.
+
.. function:: ungetch(char)
diff --git a/Lib/test/test_msvcrt.py b/Lib/test/test_msvcrt.py
index fef86ce323e54d5..a588b0ff46014a1 100644
--- a/Lib/test/test_msvcrt.py
+++ b/Lib/test/test_msvcrt.py
@@ -61,6 +61,19 @@ def test_get_osfhandle(self):
c_encoded = b'\x57\x5b' # utf-16-le (which windows internally used) encoded
char for this CJK char
+def has_console():
+ # A process created without a console (for example by pythonw.exe, or with
+ # the DETACHED_PROCESS creation flag) cannot write to the console.
+ try:
+ with open('CONOUT$', 'w'):
+ return True
+ except OSError:
+ return False
+
+
+requires_console = unittest.skipUnless(has_console(), 'requires a console')
+
+
class TestConsoleIO(unittest.TestCase):
# CREATE_NEW_CONSOLE creates a "popup" window.
@requires_resource('gui')
@@ -106,12 +119,36 @@ def test_getche(self):
def test_getwche(self):
self.check_getwch('getwche')
+ @requires_console
def test_putch(self):
msvcrt.putch(b'c')
+ @requires_console
def test_putwch(self):
msvcrt.putwch(c)
+ def test_putch_without_console(self):
+ # gh-69573: putch() and putwch() must report the error instead of
+ # silently ignoring it when the process has no console attached.
+ code = dedent('''
+ import msvcrt
+ import sys
+
+ for name, arg in (('putch', b'c'), ('putwch', 'c')):
+ func = getattr(msvcrt, name)
+ try:
+ func(arg)
+ except OSError:
+ pass
+ else:
+ sys.exit(f'msvcrt.{name}() did not raise OSError')
+ ''')
+ # DETACHED_PROCESS: the child process is created without a console.
+ proc = subprocess.run([sys.executable, '-c', code],
+ creationflags=subprocess.DETACHED_PROCESS,
+ capture_output=True, text=True)
+ self.assertEqual(proc.returncode, 0, proc.stderr)
+
class TestOther(unittest.TestCase):
def test_heap_min(self):
diff --git
a/Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst
b/Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst
new file mode 100644
index 000000000000000..441718032cc0086
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst
@@ -0,0 +1,4 @@
+:func:`msvcrt.putch` and :func:`msvcrt.putwch` now check the return value of
+the underlying ``_putch()`` and ``_putwch()`` C functions and raise
+:exc:`OSError` on failure, for example when the process has no console
+attached, instead of silently ignoring the error.
diff --git a/PC/msvcrtmodule.c b/PC/msvcrtmodule.c
index 8826e7e85a7f5ab..02f16d41b1457b1 100644
--- a/PC/msvcrtmodule.c
+++ b/PC/msvcrtmodule.c
@@ -317,6 +317,22 @@ msvcrt_getwche_impl(PyObject *module)
#endif /* MS_WINDOWS_DESKTOP */
+/* Raise an OSError for a failed _putch()/_putwch() call.
+
+ These functions fail, for example, when the process has no console
+ attached, but the CRT reports the failure without setting errno (and
+ without setting the Windows last error either), so fall back to a
+ generic error message in that case. */
+static PyObject *
+set_console_write_error(void)
+{
+ if (errno != 0) {
+ return PyErr_SetFromErrno(PyExc_OSError);
+ }
+ PyErr_SetString(PyExc_OSError, "write to console failed");
+ return NULL;
+}
+
/*[clinic input]
msvcrt.putch
@@ -330,9 +346,16 @@ static PyObject *
msvcrt_putch_impl(PyObject *module, char char_value)
/*[clinic end generated code: output=92ec9b81012d8f60 input=ec078dd10cb054d6]*/
{
+ int res;
+
_Py_BEGIN_SUPPRESS_IPH
- _putch(char_value);
+ errno = 0;
+ res = _putch(char_value);
_Py_END_SUPPRESS_IPH
+
+ if (res == EOF) {
+ return set_console_write_error();
+ }
Py_RETURN_NONE;
}
@@ -351,11 +374,17 @@ static PyObject *
msvcrt_putwch_impl(PyObject *module, int unicode_char)
/*[clinic end generated code: output=a3bd1a8951d28eee input=996ccd0bbcbac4c3]*/
{
+ wint_t res;
+
_Py_BEGIN_SUPPRESS_IPH
- _putwch(unicode_char);
+ errno = 0;
+ res = _putwch(unicode_char);
_Py_END_SUPPRESS_IPH
- Py_RETURN_NONE;
+ if (res == WEOF) {
+ return set_console_write_error();
+ }
+ Py_RETURN_NONE;
}
#endif /* MS_WINDOWS_DESKTOP */
_______________________________________________
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]