Summary
Under pytest 9.0.3 + Python 3.14.3 on Windows 11, certain test invocations (where collection short-circuits inside a .dot-prefix directory) crash during config teardown with:
File "_pytest/capture.py", line 591, in snap
self.tmpfile.seek(0)
ValueError: I/O operation on closed file.
The traceback originates from _ensure_unconfigure → stop_global_capturing → pop_outerr_to_orig → readouterr → snap → tmpfile.seek(0). The underlying tmpfile is already closed by the time snap() tries to seek to position 0.
Impact is cosmetic — tests that do run still pass, but: (a) test-discovery output is corrupted by the traceback, and (b) the process exits with code 1 (capture teardown failure) instead of the expected exit code 5 (no-tests-collected). This makes CI scripting fragile against exit-code interpretation.
Workaround: -s / --capture=no suppresses the crash entirely (no global capture is set up so nothing to seek into).
Versions
$ python --version
Python 3.14.3
$ python -m pytest --version
pytest 9.0.3
$ python -m pip list # test-relevant subset
Package Version
----------------------------- ----------
anyio 4.13.0
colorama 0.4.6
httpx 0.28.1
iniconfig 2.3.0
packaging 26.2
pluggy 1.6.0
pytest 9.0.3
pytest-asyncio 1.3.0
pytest-httpx 0.36.2
OS: Microsoft Windows 11 Pro (10.0.26200), running pytest via PowerShell 5.1.
Shell: same crash also seen invoking pytest from MSYS git-bash on the same host.
Reproducer (~100% in my environment)
Any pytest invocation against a tests/ directory that lives inside a path segment beginning with . AND that contains both __init__.py and test_*.py files which the collector cannot import in that path — the collector short-circuits in ~0.06s and the teardown crash fires.
The concrete directory I hit it on is C:\Users\Administrator\.claude\skills\actionmail\tests\:
tests/
├── __init__.py (empty)
├── test_v3_heuristics.py (does `sys.path.insert(0, SKILL_DIR); import run`)
├── test_v3_integration.py (same pattern)
└── fixtures/
└── state_2026_05_27.json
Invocation that reproduces 100%:
python -m pytest "C:\Users\Administrator\.claude\skills\actionmail\tests\" -v
Same invocation with --collect-only also reproduces (so it's collection-time, not test-run-time).
Full traceback
============================= test session starts =============================
platform win32 -- Python 3.14.3, pytest-9.0.3, pluggy-1.6.0 -- C:\Python314\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\Administrator
plugins: anyio-4.13.0, asyncio-1.3.0, httpx-0.36.2
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collecting ... Traceback (most recent call last):
File "<frozen runpy>", line 198, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\pytest\__main__.py", line 9, in <module>
raise SystemExit(pytest.console_main())
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\_pytest\config\__init__.py", line 223, in console_main
code = main()
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\_pytest\config\__init__.py", line 199, in main
ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\pluggy\_hooks.py", line 512, in __call__
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\pluggy\_manager.py", line 120, in _hookexec
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\pluggy\_callers.py", line 167, in _multicall
raise exception
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\pluggy\_callers.py", line 121, in _multicall
res = hook_impl.function(*args)
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\_pytest\main.py", line 365, in pytest_cmdline_main
return wrap_session(config, _main)
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\_pytest\main.py", line 360, in wrap_session
config._ensure_unconfigure()
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\_pytest\config\__init__.py", line 1177, in _ensure_unconfigure
self._cleanup_stack.close()
File "C:\Python314\Lib\contextlib.py", line 627, in close
self.__exit__(None, None, None)
File "C:\Python314\Lib\contextlib.py", line 619, in __exit__
raise exc
File "C:\Python314\Lib\contextlib.py", line 604, in __exit__
if cb(*exc_details):
File "C:\Python314\Lib\contextlib.py", line 482, in _exit_wrapper
callback(*args, **kwds)
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\_pytest\capture.py", line 778, in stop_global_capturing
self._global_capturing.pop_outerr_to_orig()
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\_pytest\capture.py", line 659, in pop_outerr_to_orig
out, err = self.readouterr()
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\_pytest\capture.py", line 706, in readouterr
out = self.out.snap() if self.out else ""
File "C:\Users\Administrator\AppData\Roaming\Python\Python314\site-packages\_pytest\capture.py", line 591, in snap
self.tmpfile.seek(0)
ValueError: I/O operation on closed file.
collected 0 items
============================ no tests ran in 0.06s ============================
Two characteristics stand out:
collected 0 items and no tests ran in 0.06s — collection is much faster (~30×) than the normal 1.5s collection cycle on this machine, suggesting pytest aborts the collection walk early.
- The traceback prints in the middle of the session header (between
collecting ... and collected 0 items), which is consistent with the exception being raised from _ensure_unconfigure → context-manager unwind during wrap_session teardown.
Diagnostic contrasts (which scenarios trigger vs don't)
| Scenario |
Path under |
Layout |
Result |
| A |
non-dot path |
1 passing test, no __init__.py |
✅ exit 0, no crash |
| B |
non-dot path |
empty dir |
⚠️ exit 5 (no tests collected), no crash |
| C |
non-dot path |
__init__.py only, no test files |
⚠️ exit 5, no crash |
| D |
non-dot path |
__init__.py + test with ModuleNotFoundError |
⚠️ exit 2 (collection error), clean error report, no crash |
| E |
dot-prefix path (.dotprefix/tests/) |
__init__.py + 1 passing test |
✅ exit 0, no crash |
| F |
dot-prefix path (.dotprefix/tests/) |
__init__.py + test with ModuleNotFoundError |
⚠️ exit 2, clean error report, no crash |
| G |
non-dot path (copy of the actionmail tree) |
__init__.py + 2 tests with import run → ModuleNotFoundError (sys.path mangle) + a fixtures/ sibling |
⚠️ exit 2, clean error report, no crash |
| H (REPRODUCER) |
.claude/skills/actionmail/tests/ |
same tree as G but at dot-prefix path |
❌ exit 1 with the ValueError teardown crash |
So the trigger isn't simply "dot-prefix path" (E and F don't fire), nor "collection-time import error" (D doesn't fire), nor "the tree contents" (G doesn't fire). It requires the combination of (a) the dot-prefix path and (b) the specific tree layout (multiple test modules + __init__.py + a sibling fixtures/ dir + sys.path.insert + cross-module imports). I haven't been able to reduce H further without losing the trigger.
I'm happy to tarball H and attach it if a maintainer wants the exact tree.
What I think is happening (speculative)
stop_global_capturing runs in the _cleanup_stack of Config._ensure_unconfigure(). Under Python 3.14, something is closing the underlying tmpfile of the global FDCapture.out between the time collection short-circuits and the time the cleanup stack unwinds. Python 3.14 has tightened file-lifecycle enforcement (e.g. PEP 770 ResourceWarning escalation, stricter tempfile cleanup, and the gc changes around finalizers running earlier in interpreter shutdown). One of those tightenings may be finalizing the capture tmpfile before pytest's own teardown gets to flush it.
The pre-3.14 behavior was probably "tmpfile still open at teardown, seek(0) succeeds, even if we read nothing". 3.14 turns that into a crash.
It's plausibly fixable by guarding the seek(0) in _pytest/capture.py:591 with a try/except ValueError (treat already-closed tmpfile as "nothing captured"), but I haven't validated that it'd be correct in all cases.
Workaround for users hitting this today
python -m pytest <path> -s
# or equivalently:
python -m pytest <path> --capture=no
Both disable the global capture mechanism, so there's no tmpfile to close-then-seek. Functional tests still run; you lose the captured stdout/stderr that pytest would have shown on failures.
Happy to provide:
- A tarball of the reproducer tree
- A
pip list from a fresh venv if the plugin set above matters
- Test runs against pytest
main or any candidate fix if you point me at a branch
Thanks for pytest! It's been the backbone of every Python project I've touched.
Summary
Under pytest 9.0.3 + Python 3.14.3 on Windows 11, certain test invocations (where collection short-circuits inside a
.dot-prefixdirectory) crash during config teardown with:The traceback originates from
_ensure_unconfigure → stop_global_capturing → pop_outerr_to_orig → readouterr → snap → tmpfile.seek(0). The underlyingtmpfileis already closed by the timesnap()tries to seek to position 0.Impact is cosmetic — tests that do run still pass, but: (a) test-discovery output is corrupted by the traceback, and (b) the process exits with code 1 (capture teardown failure) instead of the expected exit code 5 (no-tests-collected). This makes CI scripting fragile against exit-code interpretation.
Workaround:
-s/--capture=nosuppresses the crash entirely (no global capture is set up so nothing to seek into).Versions
OS: Microsoft Windows 11 Pro (10.0.26200), running pytest via PowerShell 5.1.
Shell: same crash also seen invoking pytest from MSYS git-bash on the same host.
Reproducer (~100% in my environment)
Any pytest invocation against a
tests/directory that lives inside a path segment beginning with.AND that contains both__init__.pyandtest_*.pyfiles which the collector cannot import in that path — the collector short-circuits in ~0.06s and the teardown crash fires.The concrete directory I hit it on is
C:\Users\Administrator\.claude\skills\actionmail\tests\:Invocation that reproduces 100%:
Same invocation with
--collect-onlyalso reproduces (so it's collection-time, not test-run-time).Full traceback
Two characteristics stand out:
collected 0 itemsandno tests ran in 0.06s— collection is much faster (~30×) than the normal 1.5s collection cycle on this machine, suggesting pytest aborts the collection walk early.collecting ...andcollected 0 items), which is consistent with the exception being raised from_ensure_unconfigure→ context-manager unwind duringwrap_sessionteardown.Diagnostic contrasts (which scenarios trigger vs don't)
__init__.py__init__.pyonly, no test files__init__.py+ test withModuleNotFoundError.dotprefix/tests/)__init__.py+ 1 passing test.dotprefix/tests/)__init__.py+ test withModuleNotFoundError__init__.py+ 2 tests withimport run→ModuleNotFoundError(sys.path mangle) + afixtures/sibling.claude/skills/actionmail/tests/So the trigger isn't simply "dot-prefix path" (E and F don't fire), nor "collection-time import error" (D doesn't fire), nor "the tree contents" (G doesn't fire). It requires the combination of (a) the dot-prefix path and (b) the specific tree layout (multiple test modules +
__init__.py+ a siblingfixtures/dir +sys.path.insert+ cross-module imports). I haven't been able to reduce H further without losing the trigger.I'm happy to tarball H and attach it if a maintainer wants the exact tree.
What I think is happening (speculative)
stop_global_capturingruns in the_cleanup_stackofConfig._ensure_unconfigure(). Under Python 3.14, something is closing the underlyingtmpfileof the globalFDCapture.outbetween the time collection short-circuits and the time the cleanup stack unwinds. Python 3.14 has tightened file-lifecycle enforcement (e.g. PEP 770 ResourceWarning escalation, strictertempfilecleanup, and thegcchanges around finalizers running earlier in interpreter shutdown). One of those tightenings may be finalizing the capture tmpfile before pytest's own teardown gets to flush it.The pre-3.14 behavior was probably "tmpfile still open at teardown, seek(0) succeeds, even if we read nothing". 3.14 turns that into a crash.
It's plausibly fixable by guarding the
seek(0)in_pytest/capture.py:591with atry/except ValueError(treat already-closed tmpfile as "nothing captured"), but I haven't validated that it'd be correct in all cases.Workaround for users hitting this today
Both disable the global capture mechanism, so there's no tmpfile to close-then-seek. Functional tests still run; you lose the captured stdout/stderr that pytest would have shown on failures.
Happy to provide:
pip listfrom a fresh venv if the plugin set above mattersmainor any candidate fix if you point me at a branchThanks for pytest! It's been the backbone of every Python project I've touched.