Skip to main content

pytest-sideeffects

ci python pypi

Find out which real files your test suite writes to — including from subprocesses your other guards cannot see.

Most test suites are less isolated than their authors believe. A helper appends to a log, a fixture rewrites a config file, a test calls main() and main() shells out to a script that does the real thing. Everything passes, so nobody looks.

$ pytest --sideeffects=audit

That is the whole setup. No fixtures to adopt, no code to change.

------------------------------- pytest-sideeffects -------------------------------
3 real file(s) changed while the suite ran:
  added     data/outbox/2026-08-17.json
  modified  logs/run.log
  modified  state/ledger.jsonl
  unattributed changes are usually a subprocess, a C extension or an atexit
  handler; run with --sideeffects=both to name the test when the write happens
  in-process.

Why another isolation plugin

Because the two mechanisms people already use each have a blind spot, and this one has both halves.

knows which test sees subprocess sees C extensions / os.system
monkeypatching the filesystem yes no no
hashing the tree before/after no yes yes
pytest-sideeffects yes (guard) yes (audit) yes (audit)

pytest-socket blocks network calls in-process. pyfakefs replaces the filesystem wholesale. Neither answers the question "did my suite change anything real?" — and neither follows a child process.

The subprocess gap is not theoretical. The incident this plugin came out of was a test that called main(); main() spawned subprocess.run([sys.executable, "poster.py", "--post-next"]); the child was a fresh interpreter with none of the parent's patches, and it published a real post to a real social account. Every in-process guard in that repo said green.

The two modes

--sideeffects=audit — what changed

Hashes every file under rootdir before and after the session and reports the difference. Process-agnostic, so it catches whatever wrote the file. Read-only: it never blocks anything. Use it to find out what you are dealing with.

--sideeffects=guard — stop it happening

Wraps open, os.open, os.remove/rename/replace/mkdir/makedirs/truncate, shutil.copy*/move/rmtree and the matching pathlib.Path methods. A write outside the allowed roots raises SideEffectBlocked, naming the test and the path:

SideEffectBlocked: pytest-sideeffects blocked open('data/ledger.jsonl') during
tests/test_poster.py::test_main: that path is outside the roots this suite may
write to. Point the code at tmp_path, or allow it with --sideeffects-allow=<glob>
if the write is intentional.

--sideeffects=both — both, and they cooperate

The guard supplies attribution for the paths it saw; the audit still reports what the guard could not reach. Anything reported by the audit but not by the guard came from outside this process — that difference is a diagnosis, not noise.

Install

pip install pytest-sideeffects

Turn it on for everyone via pyproject.toml:

[tool.pytest.ini_options]
sideeffects = "audit"
sideeffects_allow = ["build/*", "docs/_generated/*"]

In CI, make it a hard failure:

pytest --sideeffects=both --sideeffects-strict --sideeffects-json=sideeffects.json

--sideeffects-strict exits non-zero when something leaked, even if every test passed.

Options

flag effect
--sideeffects={off,audit,guard,both} mode; default off
--sideeffects-allow=GLOB path glob the suite may write to (repeatable)
--sideeffects-root=DIR tree to audit; defaults to rootdir (repeatable)
--sideeffects-strict non-zero exit when anything leaked
--sideeffects-warn-only guard records instead of raising
--sideeffects-json=PATH machine-readable report
--sideeffects-no-defaults drop the built-in allowlist entirely

Allowed out of the box, because a suite writing here surprises nobody: tmp_path and the system temp dir, __pycache__, .pytest_cache, .hypothesis, .mypy_cache, .ruff_cache, .coverage*, site-packages and the rest of the environment Python lives in, the user cache directories (~/.cache, ~/Library/Caches, %LOCALAPPDATA%), and whatever pytest itself was told to write (--junitxml, --log-file, the cache dir).

For application code: running_under_test()

A guard living in the test process cannot protect a child process. This can, because it reads an inherited environment variable — put it at the point of no return:

from pytest_sideeffects import running_under_test

def publish(post):
    if running_under_test():
        raise RuntimeError("refusing to publish from a test run")
    return api.create(post)

True when PYTEST_SIDEEFFECTS=1 (exported by this plugin for the whole session), when PYTEST_CURRENT_TEST is set (pytest's own variable, also inherited), or when pytest is imported in this process. PYTEST_SIDEEFFECTS_ALLOW=1 forces it back to False for the one test that genuinely needs the real thing.

Outside pytest there is a context manager with the same guard:

from pytest_sideeffects import no_side_effects

with no_side_effects():
    render_report()          # raises if it writes into the repo

Design notes

Content hashes, never modification times. Measured on a real repository: an mtime-based sweep reported 1450 changed files after a run; hashing the same tree over the same window reported 2. Editors, checkouts and backup agents touch mtime without changing a byte, and a tool that cries wolf 1448 times gets uninstalled. Files above 20 MB are the one exception — they are tracked by size and mtime, and the report labels them as such rather than pretending.

Measure the background before blaming a test. Some files change on their own: a daemon, a language server, a sync client. Get a control reading first:

python -m pytest_sideeffects control 40

It watches the same tree for 40 seconds with no tests running. Whatever moves there is not your suite's fault.

False positives are the real failure mode. A guard that fires on --junitxml output or on matplotlib's font cache gets switched off in one afternoon, and then it protects nothing. That is why the default allowlist is generous and the strict switch is opt-in.

Requirements

Python 3.9+, pytest 7+. No dependencies beyond pytest. Linux, macOS and Windows.

Under pytest-xdist, the audit runs in the controller process only and workers keep the guard: N workers each hashing the whole tree would cost N times as much and then each report the other workers' writes as unexplained changes.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_sideeffects-0.1.0.tar.gz (24.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_sideeffects-0.1.0-py3-none-any.whl (21.7 kB view details)

Uploaded Python 3

File details

Details for the file pytest_sideeffects-0.1.0.tar.gz.

File metadata

  • Download URL: pytest_sideeffects-0.1.0.tar.gz
  • Upload date:
  • Size: 24.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pytest_sideeffects-0.1.0.tar.gz
Algorithm Hash digest
SHA256 563563d12b5a7fe409fd76f902b9c0f96b4595e530e34acab63cf7901f034e59
MD5 c22123b068aad31fb3741c3db2b7bcb0
BLAKE2b-256 9b20775b18de5c02e79469d7c7a49cedc5f93c43bc8f9521c9c42aaa82e69dfb

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_sideeffects-0.1.0.tar.gz:

Publisher: release.yml on luandv92/pytest-sideeffects

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytest_sideeffects-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pytest_sideeffects-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 60e8fd1af602887e932416e8a79c3e8556eac5a3e26918408e5c5b2178dc265f
MD5 d2de25bf4e6a5005b4dc5a4c4a73a9dc
BLAKE2b-256 31c8c1ba72a31e87bfe28d6abe20250bc121fb8725e2db33b4cbc3841ea189f0

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytest_sideeffects-0.1.0-py3-none-any.whl:

Publisher: release.yml on luandv92/pytest-sideeffects

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page