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 */