Isolated Filesystem Testing with pytest's tmp_path Fixture
Replace hard-coded paths with pytest's tmp_path fixture to give each test a unique, isolated temporary directory. Covers basic usage, nested structures, legacy string APIs, session-scoped sharing, retention control, and common failure fixes.
05 Apr 2026, 05:55 UTC

Problem: Cross-Test Filesystem Pollution
Tests that write to hard-coded paths or shared directories create hidden dependencies. A test that leaves a file in /tmp or the project root can cause the next test to pass or fail for the wrong reason. Debugging these flaky failures wastes time and erodes confidence in the suite.
pytest's built-in tmp_path fixture solves this by giving each test function a unique, empty pathlib.Path under a session-specific temporary directory. The directory is created before the test runs and cleaned up afterward (by default, the last three session directories are retained for inspection).
Prerequisites
- pytest 6.0 or later installed (
pip install pytest) - Test functions written to accept
tmp_pathas a parameter - Code under test accepts filesystem paths as arguments (or can be configured to use a provided base directory)
Basic Usage Pattern
Add tmp_path to your test signature. Inside the test, build paths relative to it using the / operator, create directories or files, pass those paths to your code, then assert on the results.
# test_config_loader.py
from pathlib import Path
def test_loads_yaml_from_tmp_path(tmp_path: Path):
config_dir = tmp_path / "config"
config_dir.mkdir()
(config_dir / "settings.yaml").write_text("debug: true\nport: 8080")
loaded = load_config(config_dir) # your function under test
assert loaded["debug"] is True
assert loaded["port"] == 8080
The test receives a fresh Path like /tmp/pytest-of-user/pytest-123/test_loads_yaml_from_tmp_path0. No other test sees this directory.
Creating Nested Structures
Use mkdir(parents=True, exist_ok=True) to create intermediate directories in one call. Write files with write_text() or write_bytes().
def test_processes_nested_logs(tmp_path: Path):
log_root = tmp_path / "var" / "log" / "app"
log_root.mkdir(parents=True)
(log_root / "access.log").write_text("127.0.0.1 - - [01/Jan/2024] \"GET /\"\n")
(log_root / "error.log").write_text("[ERROR] connection refused\n")
stats = analyze_logs(log_root)
assert stats["total_lines"] == 2
assert stats["error_count"] == 1
Passing Paths to Legacy Code Expecting Strings
If the code under test requires str paths, wrap with str(tmp_path) or os.fspath(tmp_path). The Path object implements __fspath__() so it works transparently with open(), os.path functions, and most standard library APIs.
def test_legacy_reader(tmp_path: Path):
data_file = tmp_path / "input.dat"
data_file.write_bytes(b"\x00\x01\x02")
# legacy_read expects a string path
result = legacy_read(str(data_file))
assert result == [0, 1, 2]
Session-Scoped Temporary Directories
When multiple tests need to share a temporary directory (for example, a test suite that populates a fixture database once), use tmp_path_factory instead. It provides mktemp(basename) returning a Path that persists for the session.
# conftest.py
import pytest
from pathlib import Path
@pytest.fixture(scope="session")
def shared_temp_db(tmp_path_factory):
db_dir = tmp_path_factory.mktemp("db")
# populate once
(db_dir / "seed.sql").write_text("CREATE TABLE t (id INT);\n")
return db_dir
# test_module.py
def test_uses_shared_db(shared_temp_db: Path):
assert (shared_temp_db / "seed.sql").exists()
Clean up explicitly in a finalizer if you need removal before the session ends:
@pytest.fixture(scope="session")
def shared_temp_db(tmp_path_factory, request):
db_dir = tmp_path_factory.mktemp("db")
(db_dir / "seed.sql").write_text("CREATE TABLE t (id INT);\n")
def teardown():
import shutil
shutil.rmtree(db_dir, ignore_errors=True)
request.addfinalizer(teardown)
return db_dir
Expected Checks and Verification
- Run the test:
pytest -q test_config_loader.pyshould pass with no output beyond the summary. - Inspect the temporary path on failure: run
pytest -v --tb=short. The traceback shows the exacttmp_pathvalue, e.g.,/tmp/pytest-of-user/pytest-456/test_loads_yaml_from_tmp_path0. - Confirm isolation: add a second test that writes to the same relative name. Both should pass independently.
- Verify no leakage: after the test run, check that your project root and
/tmpcontain no test-created files (except the retained pytest session directories).
Common Failure Modes and Recovery
| Symptom | Cause | Fix |
|---|---|---|
fixture 'tmp_path' not found | Typo in parameter name or pytest < 6.0 | Spell exactly tmp_path; upgrade pytest |
FileNotFoundError for path under tmp_path | Forgot to create parent directory before writing | Call mkdir(parents=True) first |
| Test passes locally but fails in CI | Windows path length limit (260 chars) or reserved names (CON, NUL) | Keep names short; avoid reserved names; set PYTEST_DEBUG_TEMPROOT to a short path |
| Disk fills up on CI runner | Retained session directories accumulate | Set tmp_path_retention_count = 0 in pytest.ini or run pytest --override-ini=tmp_path_retention_count=0 |
| Shared state leaks between tests | Module-level variable holds a tmp_path reference | Never store tmp_path globally; always accept as parameter |
Configuration: Controlling Retention
By default pytest keeps the last three session temporary directories. In long-running CI this can consume gigabytes. Disable or reduce retention in pytest.ini:
# pytest.ini
[pytest]
tmp_path_retention_count = 0
Or via command line for a single run:
pytest --override-ini=tmp_path_retention_count=0
To keep more for debugging flaky tests, increase the count:
pytest --override-ini=tmp_path_retention_count=10
Verification Checklist
pytest --versionshows 6.0+pytest --fixtures | grep tmp_pathlists the fixture with its docstring- A minimal test writing and reading a file via
tmp_pathpasses withpytest -s -v - After the run, no unexpected files exist outside the pytest temporary root
Limitations
tmp_pathis function-scoped. It cannot be used directly in session-scoped fixtures withouttmp_path_factory.- On Windows, the base temporary path may be long (
C:\Users\name\AppData\Local\Temp\pytest-...), pushing nested paths over the 260-character limit. Mitigate by settingTMPDIRorTEMPto a short path likeC:\tmp. - If your code uses absolute paths internally (e.g.,
/etc/app/config),tmp_pathcannot redirect it without refactoring the code to accept a configurable base directory.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.