Skip to content

capture: report which test created a closed capture stream - #15215

Closed
isHeSatoshi wants to merge 1 commit into
pytest-dev:mainfrom
isHeSatoshi:fix/capture-closed-stream-message
Closed

isHeSatoshi wants to merge 1 commit into
pytest-dev:mainfrom
isHeSatoshi:fix/capture-closed-stream-message

Conversation

@isHeSatoshi

Copy link
Copy Markdown

The problem

When a test keeps a reference to a stream which pytest captures — the classic case being a
logging.StreamHandler(sys.stdout) installed by one test and never removed — writing to
that stream from a later test fails with:

ValueError: I/O operation on closed file.

That says nothing about where the stream came from, so the leak is hard to track down.
#5743 has the original report, and the agreed direction there (from @nicoddemus) is:

we don't need to actually catch the exception anywhere, we just need to raise a better
error message during write [...] so the error message will help the user finding out
about the test.

The fix

EncodedFile (fd capture: the default --capture=fd and capfd) and CaptureIO
(sys capture: capsys and --capture=sys) now share a _CaptureStream base which knows
which test created the stream, and turns a write to an already-closed stream into:

ValueError: cannot write to a stream that pytest has already closed (created by test
'test_leak.py::test_leaks_a_handler'); this usually means that a global object, such as a
logging handler, kept a reference to it after the test finished

CaptureFixture._start() passes self.request.node.nodeid down to the capture class,
which forwards it to the stream it creates. Global (session-wide) captures have no owning
test, so their message keeps the general wording without the nodeid. Encoding errors are
ValueError subclasses as well, so they are re-raised unchanged — self.closed is what
distinguishes the two cases.

This is complementary to #14981, which warns at startup on Windows when a handler holds a
stream pytest replaced before it took over; this change improves the error at the point
where the write actually fails, on every platform.

Tests

  • testing/test_capture.py::TestClosedCaptureStream — closed CaptureIO with and without
    a nodeid, closed EncodedFile, and that encoding errors are left alone.
  • testing/test_capture.py::test_write_to_stream_of_finished_test[capsys|capfd] — the
    end-to-end version: a test leaks sys.stdout, a later test writes to it and checks the
    message names the first test.

All five of these fail on main without the src/_pytest/capture.py change (verified by
reverting that file), and pass with it.

Commands run against this branch:

  • python -m pytest testing/test_capture.py testing/test_subtests.py -q →
    172 passed, 3 skipped, 1 xfailed
  • python -m pytest testing/test_terminal.py testing/test_runner.py testing/logging -q →
    398 passed, 2 skipped, 1 xfailed, with one failure in
    testing/logging/test_reporting.py::test_messages_logged caused by the machine running
    out of disk space mid-run; that test passes when re-run on its own.
  • python -m mypy src/_pytest/capture.py testing/test_capture.py → Success: no issues found
  • ruff check and ruff format --check on both files → clean

changelog/5743.improvement.rst is included.

AI assistance (disclosure per CONTRIBUTING.rst)

This change was prepared with an AI coding agent (Factory Droid) on behalf of
@isHeSatoshi, who is submitting it and will respond to review feedback. Everything above
was verified by actually running the commands shown against this branch rather than being
asserted. The agent is credited here rather than in a Co-authored-by trailer; happy to
add a trailer to the commits if that is preferred.

Fixes #5743

Writing to a stream which pytest closed (for example a ``sys.stdout``
obtained from the ``capsys`` fixture and kept alive by a logging handler)
now raises an error which names the test that created the stream, instead
of a bare ``I/O operation on closed file``.
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Oct 19, 2026
@RonnyPfannschmidt

Copy link
Copy Markdown
Member

unattended agent

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.

Improve error message when tests use closed stdout/stderr (capture)

2 participants