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

Metadata

Release files for pytest-sideeffects 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pytest-sideeffects 0.1.0
File Size Uploaded
pytest_sideeffects-0.1.0.tar.gz 24.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-sideeffects 0.1.0
File Interpreter ABI Platform
pytest_sideeffects-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 46.2 kB

Release files / pytest_sideeffects-0.1.0.tar.gz

Download URL pytest_sideeffects-0.1.0.tar.gz
Size 24.6 kB
Tags Source
SHA-256 checksum
How to use checksums
563563d12b5a7fe409fd76f902b9c0f96b4595e530e34acab63cf7901f034e59
BLAKE2b-256 checksum
How to use checksums
9b20775b18de5c02e79469d7c7a49cedc5f93c43bc8f9521c9c42aaa82e69dfb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.

Transparency log

Release files / pytest_sideeffects-0.1.0-py3-none-any.whl

Download URL pytest_sideeffects-0.1.0-py3-none-any.whl
Size 21.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
60e8fd1af602887e932416e8a79c3e8556eac5a3e26918408e5c5b2178dc265f
BLAKE2b-256 checksum
How to use checksums
31c8c1ba72a31e87bfe28d6abe20250bc121fb8725e2db33b4cbc3841ea189f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 19, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release 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