Recommended resources
The freely available resources this lesson will direct you to.
- pytest: testing env variables (intermediate) anthony explains #317www.youtube.com
- pytest: everything you need to know about fixtures (intermediate) anthony explains #487www.youtube.com
- How to use temporary directories and files in testsdocs.pytest.org
- How to monkeypatch/mock modules and environmentsdocs.pytest.org
Hello! Last lesson, you used fixtures to give tests fresh data and to clean up resources after a test finishes. Pytest also supplies fixtures for two common sources of unreliable tests: files left on disk and values inherited from the machine running the suite.
In this lesson, you’ll test a small incident-report writer. You’ll use tmp_path to keep its files out of your working tree, then monkeypatch to control the environment variable that selects where reports go. These techniques are especially useful when turning operational Python scripts into code that runs predictably both locally and in CI.
Give each filesystem test its own workspace
Suppose an incident tool writes a short text summary. A test that writes into the project directory might pass on the first run but encounter a file left by an earlier run. A hard-coded path can be worse: it may refer to a real directory on someone else’s machine.
Pytest’s tmp_path fixture gives each test invocation its own temporary directory as a pathlib.Path. Read the official pytest guide’s short example before building ours.
How to use temporary directories and files in tests
Read the pytest documentation’s “The tmp_path fixture” subsection to see what the fixture provides and how its Path methods create and inspect files.
Start with the paragraph describing a directory per test, and read through the file-creation example and the sentence immediately after it. The example’s final assert 0 is deliberately there to show failure output; do not copy it into your test.
Put this production code in src/incident_app/reporting.py:
from pathlib import Path
def write_incident_summary(
incident_id: int,
summary: str,
directory: Path,
) -> Path:
directory.mkdir(parents=True, exist_ok=True)
report = directory / f"incident-{incident_id}.txt"
report.write_text(f"{summary}\n", encoding="utf-8")
return report
The function takes its destination as an argument. That is a useful design choice, not just a testing trick: the writing logic does not need to know whether the directory came from application configuration, a command-line option, or a test.
Create tests/test_reporting.py:
from pathlib import Path
from incident_app.reporting import write_incident_summary
def test_writes_incident_summary(tmp_path: Path) -> None:
directory = tmp_path / "nested" / "reports"
report = write_incident_summary(17, "Investigating", directory)
assert report == directory / "incident-17.txt"
assert report.read_text(encoding="utf-8") == "Investigating\n"
The arrange step chooses a directory beneath tmp_path; it does not create that directory, so the test also checks that production code can create it. The act step calls the real writer. The assertions verify both the returned location and the bytes of text that another program would read.
There is no custom cleanup fixture here. Pytest manages the temporary workspace, although it may retain directories from recent runs for debugging; do not assume they vanish the instant a test ends. What matters for isolation is that another test does not receive this test’s tmp_path. Keep every test-created file beneath it rather than mixing temporary and hard-coded output paths.
Control configuration without changing the host environment
Now suppose the operational entry point chooses the report directory from INCIDENT_REPORT_DIR. A test must not depend on whether that variable happens to be set in your shell or CI runner.
Watch the selected parts of pytest: testing env variables (intermediate) by anthonywritescode. The first excerpt shows why a test tied to the presenter’s environment fails elsewhere; the second shows pytest’s temporary environment override.
pytest: testing env variables (intermediate) anthony explains #317
Watch these excerpts for the failure caused by an assumed environment value and for the monkeypatch.setenv remedy.
In the failure, focus on why a locally passing assertion is not portable. Then watch the pytest approach: the changed variable remains in effect during the test and is restored afterward.
Append the following to src/incident_app/reporting.py, adding import os alongside its existing import:
import os
def configured_report_directory() -> Path:
value = os.getenv("INCIDENT_REPORT_DIR")
if not value:
raise RuntimeError("INCIDENT_REPORT_DIR is not set")
return Path(value)
def export_incident_summary(incident_id: int, summary: str) -> Path:
return write_incident_summary(
incident_id,
summary,
configured_report_directory(),
)
configured_report_directory() reads the variable when called. It rejects both an absent and an empty value: treating an empty string as a directory could accidentally direct output into the current working directory. The entry point then passes the resulting Path to the writer you already tested.
The pytest documentation covers both setting a variable and deliberately removing one.
How to monkeypatch/mock modules and environments
Read the official pytest documentation’s “Monkeypatching environment variables” subsection for the setenv and delenv cases.
Begin with the paragraph about changing environment values. Read through both test examples just before the paragraph about moving this behavior into fixtures. Notice raising=False on delenv: the test can request an absent variable even if it was already absent.
Add import pytest and export_incident_summary to the imports in tests/test_reporting.py, then append these tests:
def test_export_uses_configured_temp_directory(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
directory = tmp_path / "exports"
monkeypatch.setenv("INCIDENT_REPORT_DIR", str(directory))
report = export_incident_summary(23, "Mitigated")
assert report == directory / "incident-23.txt"
assert report.read_text(encoding="utf-8") == "Mitigated\n"
def test_export_requires_directory_when_variable_is_absent(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("INCIDENT_REPORT_DIR", raising=False)
with pytest.raises(
RuntimeError, match="INCIDENT_REPORT_DIR is not set"
):
export_incident_summary(23, "Mitigated")
def test_export_rejects_empty_directory(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("INCIDENT_REPORT_DIR", "")
with pytest.raises(
RuntimeError, match="INCIDENT_REPORT_DIR is not set"
):
export_incident_summary(23, "Mitigated")
setenv needs a string, hence str(directory); the filesystem assertions still use Path. The first test controls both sides of the boundary: the environment variable points to a directory belonging to that test, and the resulting file is inspected there. The other tests establish the missing and empty configuration cases regardless of the host’s original value.
Pytest restores changes made through monkeypatch after each test, including when an assertion fails. You do not need to save and restore the previous environment value yourself.
Keep the isolation boundary in the right place
Patch configuration before calling code that reads it. If a module instead evaluates os.getenv("INCIDENT_REPORT_DIR") at import time and stores the result in a constant, setting the variable inside a test will not update that stored value. Reading configuration in configured_report_directory() makes the dependency explicit at the point of use.
For the same reason, prefer passing a Path directly to functions that can accept one, as write_incident_summary() does. Reserve environment-variable setup for tests of the code that actually reads the environment. This keeps a test of text-file writing from also depending on configuration behavior.
Run the tests from the project root:
python -m pytest -v tests/test_reporting.py
If a filesystem test behaves differently in CI, inspect every path it creates: tmp_path isolates only files placed beneath that path. If an environment test behaves differently, check whether the variable was patched before its lookup and whether the code cached its value earlier. Those questions usually locate the leak more quickly than adding cleanup after the test.
Takeaways
Use tmp_path when a test needs real file I/O but must not touch a shared directory. Use monkeypatch.setenv and monkeypatch.delenv to make environment-dependent branches deterministic without changing the runner’s lasting environment. Together, they let you test the actual report-writing behavior under controlled conditions.
Next, you’ll isolate a different kind of boundary: replacing an external collaborator with a focused test double rather than calling a real service.