pytest tmp_path

Introduction

A test that reads a file, writes a report or builds a project needs a place for its files. Writing them next to the test pollutes the repository and makes one run depend on the previous one. Writing them under /tmp by hand means inventing a unique name and cleaning up on failure.

The tmp_path fixture of pytest solves all of it: every test that asks for it gets a new empty directory as a Python pathlib.Path, and pytest removes the directory later.

One directory per test

The fixture is used by naming it in the test signature. A file is created with the usual / operator and the usual write_text().

In the file test_tmp_path.py:

import json def test_one_directory_per_test(tmp_path): first = tmp_path / "paths.txt" first.write_text("/about/\n", encoding="utf-8") assert first.read_text(encoding="utf-8") == "/about/\n" def test_a_second_test_gets_an_empty_directory(tmp_path): assert list(tmp_path.iterdir()) == [] nested = tmp_path / "data" / "index.json" nested.parent.mkdir() nested.write_text(json.dumps({"pages": 2}), encoding="utf-8") assert json.loads(nested.read_text(encoding="utf-8")) == {"pages": 2}

python -m pytest test_tmp_path.py -v

============================= test session starts ============================== platform linux -- Python 3.12.16, pytest-9.2.0, pluggy-1.7.0 -- … cachedir: .pytest_cache rootdir: <project> plugins: allure-pytest-2.16.2 collecting ... collected 2 items test_tmp_path.py::test_one_directory_per_test PASSED [ 50%] test_tmp_path.py::test_a_second_test_gets_an_empty_directory PASSED [100%] ============================== 2 passed in 0.04s ===============================

The second test proves the point: its directory is empty even though the first test wrote a file in its own. Every test starts from an empty directory, and neither test had to name it.

Files and subdirectories

tmp_path is an ordinary Path, so everything from pathlib works:

tmp_path_factory: one directory per session

A fixture that is shared by many tests should not be built once per test. tmp_path_factory makes directories that live as long as the session fixture that created them:

import pytest @pytest.fixture(scope="session") def index_file(tmp_path_factory): folder = tmp_path_factory.mktemp("index") path = folder / "index.json" path.write_text('{"pages": 3}', encoding="utf-8") return path def test_index_is_readable(index_file): assert index_file.read_text(encoding="utf-8") == '{"pages": 3}'

python -m pytest test_tmp_path_factory.py -v

============================= test session starts ============================== platform linux -- Python 3.12.16, pytest-9.2.0, pluggy-1.7.0 -- … cachedir: .pytest_cache rootdir: <project> plugins: allure-pytest-2.16.2 collecting ... collected 1 item test_tmp_path_factory.py::test_index_is_readable PASSED [100%] ============================== 1 passed in 0.03s ===============================

mktemp("index") creates a new directory with the given prefix inside the session base directory and returns its Path. Use the session factory only for data that is expensive to prepare and safe to share; a test that changes the file needs its own tmp_path.

tmp_path in the repo

The smallest test file of the suite writes the path list of the sitemap tests into tmp_path: container/tests/test_url_selection.py uses it together with monkeypatch and importlib.reload:

def test_listed_paths_match_exactly_and_index_php_equals_directory(monkeypatch, tmp_path): listed = tmp_path / "paths.txt" listed.write_text("/about/\n/ru/qa/page.php\n# comment\nnot-a-path\n", encoding="utf-8") selection = _reload(monkeypatch, str(listed))

The variable has to hold a string, so the path is passed through str(). The full walk-through is Testing a module that reads a setting at import .

pytest keeps the last few run directories, so after a failure the printed path still exists and can be inspected. Nothing in the repository is touched, and two tests that use the same file name never see each other.

Article author: Arthur Isaev