Mathew K Analytics

Lesson 13 · Pytest deep dive

pytest Tutorial #13: Temporary Files & Directories

On testing real Python code with pytest. tmppath and tmppathfactory: real disk-backed scratch space, auto-cleaned. Let's get into it. tmppath is a built-in…

⬇ Download notebook

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

pytest Deep-Dive, Video 13: Temporary Files#

  • Video thirteen of an eighteen-part series on testing real Python code with pytest.
  • tmp_path and tmp_path_factory: real disk-backed scratch space, auto-cleaned.
  • Let's get into it.

Part 1: The Helper From Last Time#

import pytest, tempfile, os
def run_pytest(code_str, args=None):
    with tempfile.NamedTemporaryFile(mode='w', suffix='_test.py', delete=False) as f:
        f.write(code_str)
        path = f.name
    result = pytest.main([path, *(args or [])])
    os.unlink(path)
    return result

Part 2: tmp_path - a Fresh pathlib.Path Per Test#

code = '''
def test_tmp_path_is_a_directory(tmp_path):
    assert tmp_path.is_dir()
    assert tmp_path.exists()

def test_tmp_path_starts_empty(tmp_path):
    assert list(tmp_path.iterdir()) == []
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpuv7z1w06_test.py::test_tmp_path_is_a_directory PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpuv7z1w06_test.py::test_tmp_path_starts_empty PASSED [100%]

============================== 2 passed in 0.23s ==============================
0

Part 3: Writing and Reading Real Files#

code = '''
def test_write_and_read_file(tmp_path):
    config_file = tmp_path / 'config.txt'
    config_file.write_text('debug=true')
    assert config_file.exists()
    assert config_file.read_text() == 'debug=true'

def test_write_creates_real_file_on_disk(tmp_path):
    data_file = tmp_path / 'data.csv'
    data_file.write_text('a,b,c\\n1,2,3\\n')
    assert data_file.stat().st_size > 0
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpv3l0phl5_test.py::test_write_and_read_file PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpv3l0phl5_test.py::test_write_creates_real_file_on_disk PASSED [100%]

============================== 2 passed in 0.13s ==============================
0

Part 4: Every Test Gets Its Own, Genuinely Unique tmp_path#

code = '''
seen_paths = []

def test_first_records_path(tmp_path):
    seen_paths.append(str(tmp_path))
    (tmp_path / 'marker.txt').write_text('first')

def test_second_records_path(tmp_path):
    seen_paths.append(str(tmp_path))
    marker = tmp_path / 'marker.txt'
    assert not marker.exists()

def test_paths_were_different():
    assert seen_paths[0] != seen_paths[1]
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 3 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpwd3dufn0_test.py::test_first_records_path PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpwd3dufn0_test.py::test_second_records_path PASSED [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpwd3dufn0_test.py::test_paths_were_different PASSED [100%]

============================== 3 passed in 0.13s ==============================
0

Part 5: tmp_path_factory - Session-Scoped Temp Directories#

code = '''
import pytest

@pytest.fixture(scope='session')
def shared_data_dir(tmp_path_factory):
    d = tmp_path_factory.mktemp('shared_data')
    (d / 'lookup.txt').write_text('42')
    return d

def test_reads_shared_data_first(shared_data_dir):
    assert (shared_data_dir / 'lookup.txt').read_text() == '42'

def test_reads_shared_data_second(shared_data_dir):
    assert (shared_data_dir / 'lookup.txt').exists()
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpmt_h93f8_test.py::test_reads_shared_data_first PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpmt_h93f8_test.py::test_reads_shared_data_second PASSED [100%]

============================== 2 passed in 0.12s ==============================
0

Part 6: A Real Pattern - Testing Config File Round-Trips#

code = '''
import json

def save_config(path, config):
    path.write_text(json.dumps(config))

def load_config(path):
    if not path.exists():
        return {}
    return json.loads(path.read_text())

def test_save_then_load_round_trips(tmp_path):
    config_path = tmp_path / 'settings.json'
    original = {'debug': True, 'workers': 4}
    save_config(config_path, original)
    loaded = load_config(config_path)
    assert loaded == original

def test_load_missing_file_returns_empty(tmp_path):
    missing_path = tmp_path / 'does_not_exist.json'
    assert load_config(missing_path) == {}
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
============================= test session starts =============================
collecting ... collected 2 items

..\..\..\Users\makmw\AppData\Local\Temp\tmposbdyeiq_test.py::test_save_then_load_round_trips PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmposbdyeiq_test.py::test_load_missing_file_returns_empty PASSED [100%]

============================== 2 passed in 0.13s ==============================
0

Wrap-Up: What You Learned#

  • tmp_path is a built-in fixture handing each test its own fresh, empty pathlib.Path directory.
  • The / operator builds paths inside it; write_text and read_text perform real disk I/O.
  • tmp_path is function-scoped by default, so different tests never share or collide over the same directory.
  • tmp_path_factory is session-scoped, supporting several distinct temp directories that persist across tests.
  • tmp_path_factory.mktemp(name) creates one named, persistent subdirectory for shared session-level setup.
  • Testing file I/O against tmp_path keeps a suite fast, isolated, and safe from touching real user files.
  • That wraps up temporary files. Next up: Capturing Output.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.