Repository navigation
mode 0o000 directory causes following test suites with -W error to fail #15169
Description
(Sorry for the wall of text, it is a very specific edge case)
When running a pytest test suite that creates a directory with mode 0o000 in it's tmp_path and pytest attempts to clean up that directory (seems to happen only after running pytest a few times) pytest shows a warning that it could not remove the tmp_path due to "Directory not empty". Test suites which consider warnings as errors (eg. using -W error) can fail due to this even if the tests are all correct.
Ideally rm_rf() in _pytest/pathlib.py would set the S_IRUSR|S_IWUSR|S_IXUSR bits of directories while recursing. I have not tried it yet but I suspect that the on_rm_rf_error handler runs to late to re-try recursing into directories.
Depending on a condition that I couldn't figure out yet the error is ether about scandir or Directory not empty (both error messages are below). In the latter case it seems like the directory with mode 0o000 is silently ignored causing the parent directory to not be removable.
A minimal shell script to reproduce the issue:
cd "$(mktemp -d)"
export TMPDIR="$PWD"
mkdir tests
cat << EOF > tests/test.py
def test_foo(tmp_path: Path) -> None:
(tmp_path / "mode-0").mkdir(mode=0o000)
EOF
# passes
pytest tests/test.py
# passes, because pytest is not yet attempting to remove the mod 0o000 directory
pytest -W error tests/test.py
pytest -W error tests/test.py
# fails
pytest -W error tests/test.pyA testsuite which creates such a directory can be found here: https://github.com/tox-dev/python-discovery/blob/469741a689fee16fe41135335d5cc10a767dd16c/tests/test_discovery.py#L78
This happens on alpine linux edge with python 3.14.8 and py3-pytest 9.1.1 but I am relatively certain that this is not relevant to the issue.
tests/test.py . [100%]Traceback (most recent call last):
File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 367, in cleanup_numbered_dir
try_cleanup(path, consider_lock_dead_if_created_before)
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ResourceWarning: unclosed scandir iterator <posix.ScandirIterator object at 0x7f323c078230>
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/usr/bin/pytest", line 8, in <module>
sys.exit(_console_main())
~~~~~~~~~~~~~^^
File "/usr/lib/python3.14/site-packages/_pytest/config/__init__.py", line 253, in _console_main
code = _main(prog=_get_prog_name(sys.argv))
File "/usr/lib/python3.14/site-packages/_pytest/config/__init__.py", line 235, in _main
config._ensure_unconfigure()
~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "/usr/lib/python3.14/site-packages/_pytest/config/__init__.py", line 1212, in _ensure_unconfigure
self.hook.pytest_unconfigure(config=self)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/pluggy/_hooks.py", line 512, in __call__
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/pluggy/_manager.py", line 120, in _hookexec
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 167, in _multicall
raise exception
File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 121, in _multicall
res = hook_impl.function(*args)
File "/usr/lib/python3.14/site-packages/_pytest/unraisableexception.py", line 173, in pytest_unconfigure
collect_unraisable(config)
~~~~~~~~~~~~~~~~~~^^^^^^^^
File "/usr/lib/python3.14/site-packages/_pytest/unraisableexception.py", line 79, in collect_unraisable
raise errors[0]
File "/usr/lib/python3.14/site-packages/_pytest/unraisableexception.py", line 67, in collect_unraisable
warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pytest.PytestUnraisableExceptionWarning: Exception ignored while finalizing scandir iterator <posix.ScandirIterator object at 0x7f323c078230>: None
Traceback (most recent call last):
File "/usr/lib/python3.14/shutil.py", line 753, in _rmtree_safe_fd_step
os.rmdir(name, dir_fd=dirfd)
~~~~~~~~^^^^^^^^^^^^^^^^^^^^
OSError: [Errno 39] Directory not empty: '/run/user/1000/tmp/pytest-of-user/garbage-e3fd6d00-0296-4405-9580-9573874b9999/test_discovery_via_path_in_non0'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<frozen runpy>", line 203, in _run_module_as_main
File "<frozen runpy>", line 88, in _run_code
File "/usr/lib/python3.14/site-packages/pytest/__main__.py", line 9, in <module>
raise SystemExit(_console_main())
~~~~~~~~~~~~~^^
File "/usr/lib/python3.14/site-packages/_pytest/config/__init__.py", line 253, in _console_main
code = _main(prog=_get_prog_name(sys.argv))
File "/usr/lib/python3.14/site-packages/_pytest/config/__init__.py", line 229, in _main
ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/pluggy/_hooks.py", line 512, in __call__
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/pluggy/_manager.py", line 120, in _hookexec
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 167, in _multicall
raise exception
File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 121, in _multicall
res = hook_impl.function(*args)
File "/usr/lib/python3.14/site-packages/_pytest/main.py", line 377, in pytest_cmdline_main
return wrap_session(config, _main)
File "/usr/lib/python3.14/site-packages/_pytest/main.py", line 365, in wrap_session
config.hook.pytest_sessionfinish(
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
session=session, exitstatus=session.exitstatus
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/usr/lib/python3.14/site-packages/pluggy/_hooks.py", line 512, in __call__
return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/pluggy/_manager.py", line 120, in _hookexec
return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 167, in _multicall
raise exception
File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 139, in _multicall
teardown.throw(exception)
~~~~~~~~~~~~~~^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/_pytest/logging.py", line 888, in pytest_sessionfinish
return (yield)
^^^^^
File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 139, in _multicall
teardown.throw(exception)
~~~~~~~~~~~~~~^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/_pytest/terminal.py", line 961, in pytest_sessionfinish
result = yield
^^^^^
File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 139, in _multicall
teardown.throw(exception)
~~~~~~~~~~~~~~^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/_pytest/warnings.py", line 119, in pytest_sessionfinish
return (yield)
^^^^^
File "/usr/lib/python3.14/site-packages/pluggy/_callers.py", line 121, in _multicall
res = hook_impl.function(*args)
File "/usr/lib/python3.14/site-packages/_pytest/tmpdir.py", line 340, in pytest_sessionfinish
tmp_path_factory._exit_stack.close()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "/usr/lib/python3.14/contextlib.py", line 627, in close
self.__exit__(None, None, None)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/contextlib.py", line 619, in __exit__
raise exc
File "/usr/lib/python3.14/contextlib.py", line 604, in __exit__
if cb(*exc_details):
~~^^^^^^^^^^^^^^
File "/usr/lib/python3.14/contextlib.py", line 482, in _exit_wrapper
callback(*args, **kwds)
~~~~~~~~^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 369, in cleanup_numbered_dir
try_cleanup(path, consider_lock_dead_if_created_before)
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 338, in try_cleanup
maybe_delete_a_numbered_dir(path)
~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 290, in maybe_delete_a_numbered_dir
rm_rf(garbage)
~~~~~^^^^^^^^^
File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 167, in rm_rf
shutil.rmtree(str(path), onexc=onerror)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/shutil.py", line 852, in rmtree
_rmtree_impl(path, dir_fd, onexc)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/shutil.py", line 721, in _rmtree_safe_fd
_rmtree_safe_fd_step(stack, onexc)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
File "/usr/lib/python3.14/shutil.py", line 802, in _rmtree_safe_fd_step
onexc(func, path, err)
~~~~~^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/site-packages/_pytest/pathlib.py", line 95, in on_rm_rf_error
warnings.warn(
~~~~~~~~~~~~~^
PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
pytest.PytestWarning: (rm_rf) error removing /run/user/1000/tmp/pytest-of-user/garbage-e3fd6d00-0296-4405-9580-9573874b9999/test_discovery_via_path_in_non0
<class 'OSError'>: [Errno 39] Directory not empty: '/run/user/1000/tmp/pytest-of-user/garbage-e3fd6d00-0296-4405-9580-9573874b9999/test_discovery_via_path_in_non0'
Activity
sertonix commentedon Oct 13, 2026
Not sure how I missed this but it looks like #14331 already fixed the issue on the main branch.
The scandir iterator fix from #15171 might still be relevant but I don't know how to determine that.
main/py3-pytest: backport rm_rf with dir mode 0 fix