Skip to content

Reopen sys.__std*__ on Windows consoles so they follow fd capture#14736

Open
Abhishek-kroy wants to merge 1 commit into
pytest-dev:mainfrom
Abhishek-kroy:fix-12349-windows-dunder-streams
Open

Reopen sys.__std*__ on Windows consoles so they follow fd capture#14736
Abhishek-kroy wants to merge 1 commit into
pytest-dev:mainfrom
Abhishek-kroy:fix-12349-windows-dunder-streams

Conversation

@Abhishek-kroy

Copy link
Copy Markdown

Closes #12349

The problem

On Windows, when attached to a console, sys.__stdin__/sys.__stdout__/sys.__stderr__ are backed by _io._WindowsConsoleIO, which writes through the console handle rather than the file descriptor, and whose isatty() unconditionally returns True.

So while FDCapture has dup2-ed the fds away (--capture=fd, the default):

  • sys.__stdout__.isatty() keeps returning True, while os.isatty(1) correctly returns False — unlike POSIX, where both return False (this is the exact assertion failure reported in sys.__stdout__.isatty() returns incorrect value for Windows 11 #12349, reproduced on Windows 11);
  • writes to sys.__stdout__ either escape capturing and are printed straight to the terminal, or fail with OSError: [WinError 6] The handle is invalid once dup2 has closed the console handle (the very handle instability already described in _windowsconsoleio_workaround's docstring).

The fix

Extend _windowsconsoleio_workaround to also reopen the dunder streams, backed by their original file descriptors (unlike sys.std*, which keep being reopened on duplicated fds for handle isolation). The raw stream is constructed explicitly with io.FileIO, because open() on a console fd produces another _WindowsConsoleIO and would change nothing.

After the fix the dunder streams follow fd redirection exactly like on POSIX: isatty() reflects the actual state of the fd, writes during capture land in the capture file, and writes outside capture reach the console. sys.__stdin__.encoding and sys.__stderr__.fileno() (used by the faulthandler plugin) are preserved.

Verification on Windows 11

Running the reproducer from #12349 under a real console (ConPTY), before the fix:

FAILED - AssertionError: assert not True   (sys.__stdout__.isatty())

and a write probe showed OSError(9, 'The handle is invalid') plus output leaking to the terminal despite capture being active.

After the fix, both asserts from the issue pass, and writes to sys.__stdout__ are captured. testing/test_capture.py (131 tests), testing/test_terminal.py and testing/test_faulthandler.py all pass both piped and under a real console on Windows 11.

The new regression test fakes the console environment (monkeypatched sys.platform and io._WindowsConsoleIO), so it exercises this code path on all platforms/CI runners, where no real console is available.


  • Include documentation when adding new features.
  • Include new tests or update existing tests when applicable.
  • Allow maintainers to push and squash when merging my commits. Please uncheck this if you prefer to squash the commits yourself.
  • Add text like closes #XYZW to the PR description and/or commits (where XYZW is the issue number).
  • If AI agents were used, they are credited in Co-authored-by commit trailers.
  • Create a new changelog file in the changelog directory (changelog/12349.bugfix.rst).
  • Add yourself to AUTHORS in alphabetical order.

Disclosure per the AI/LLM-Assisted Contributions Policy: this change was developed with AI assistance (Claude Code), credited in the commit trailer. I take responsibility for the change and will respond to review feedback myself.

On Windows, when attached to a console, the sys.__std{in,out,err}__
streams are backed by _WindowsConsoleIO, which writes through the
console handle rather than the file descriptor and whose isatty()
unconditionally returns True. As a result, while FDCapture had
redirected the fds, sys.__stdout__.isatty() kept returning True (unlike
on POSIX, where it returns False), and writes to sys.__stdout__ either
escaped capturing and appeared on the terminal or failed with
"OSError: [WinError 6] The handle is invalid" once dup2 had closed the
console handle.

Extend _windowsconsoleio_workaround to also reopen the dunder streams,
backed by their original file descriptors. The raw stream is built
explicitly with io.FileIO because open() on a console fd would produce
another _WindowsConsoleIO.

Closes pytest-dev#12349

Co-authored-by: Claude <noreply@anthropic.com>
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sys.__stdout__.isatty() returns incorrect value for Windows 11

1 participant