An autouse fixture runs for every test in its scope without any test asking for it. That is precisely its value — a network block, a reset of a global registry, a leak check that no test should be able to forget — and precisely its danger. Autouse fixtures are invisible at the call site, so a test's behaviour depends on code the reader cannot see from the test, and a suite that accumulates them gradually turns into one where nobody can say what state a given test starts in.
The discipline that keeps them useful is short: autouse only for behaviour that is universal and invisible to test logic, placed in the narrowest conftest.py that covers the tests needing it, cheap enough that paying for it everywhere is acceptable, and with a documented opt-out for the rare exception. This guide covers each of those, plus the audit that finds the autouse fixtures that should not be.
Prerequisites
pytest >= 8.0.- The conftest hierarchy rules from managing conftest hierarchies, since where an autouse fixture lives decides which tests it affects.
--fixtures-per-testand--setup-show, both built into pytest, for auditing.
Solution
Reserve autouse for universal guards, place them narrowly, and give each an explicit opt-out.
# tests/conftest.py — universal and invisible: every test should have it.
import socket
import pytest
@pytest.fixture(autouse=True)
def block_network(request, monkeypatch):
# Opt-out: tests that genuinely need the network say so explicitly.
if request.node.get_closest_marker("allow_network"):
return
def guard(*args, **kwargs):
raise RuntimeError(f"network access in {request.node.nodeid}")
monkeypatch.setattr(socket.socket, "connect", guard)
# tests/integration/conftest.py — narrow: only integration tests need it.
import pytest
@pytest.fixture(autouse=True)
def clean_message_bus(bus):
yield
bus.purge() # every integration test leaves the bus empty
# A test that needs the exception declares it, visibly.
import pytest
@pytest.mark.allow_network
def test_downloads_the_public_schema(http):
assert http.get("https://schema.example/openapi.json").ok
The first fixture is a guard every test in the suite should have, and its opt-out is a marker that appears on the test — so the exception is visible exactly where it applies. The second lives in the integration subtree, so unit tests never pay for a message bus they do not use.
Why this works
pytest collects autouse fixtures from every conftest.py between the rootdir and a test's directory, and activates each one for every test in that subtree. The fixture's scope and its location therefore together decide its reach: a function-scoped autouse fixture in the root conftest.py runs before every single test in the suite, while the same fixture in tests/integration/conftest.py runs only for integration tests.
The marker-based opt-out works because request.node is the test item, and get_closest_marker finds a marker on the test, its class or its module. The fixture can therefore make its behaviour conditional on something the test declares, which is the only way a test can influence a fixture it did not request — and it keeps the exception visible in the test's own source.
Edge cases and failure modes
- Autouse fixtures that do I/O. A database reset that takes 20 ms, autouse at the root, adds 20 seconds to a thousand-test suite — including unit tests that never touch the database. Move it to the subtree that needs it.
- Autouse fixtures that return values. No test requested it, so no test can use the value. If tests need the value, the fixture should not be autouse.
- Order dependence between autouse fixtures. Autouse fixtures in the same file run in definition order; across files, root first. Relying on that order is fragile; make one depend on the other explicitly.
- Session-scoped autouse fixtures. They run once, before the first test in scope, which may be much later than expected when running a subset. Do not rely on them for side effects a test needs.
- Opt-outs used widely. If a third of the tests carry the opt-out marker, the behaviour is not universal and should become an explicit fixture.
The cost that autouse hides
The most common problem with autouse fixtures is not correctness but cost, and it is invisible precisely because nobody requested the fixture. A single function-scoped autouse fixture in the root conftest.py that takes fifteen milliseconds — a database truncate, a cache flush, a temporary directory copy — costs fifteen seconds on a thousand-test suite, and it costs the unit tests that never touched the database exactly as much as the integration tests that did.
--durations does not attribute that time clearly, because setup time is reported per test and every test's setup is fifteen milliseconds slower. The suite simply looks uniformly slow, and the usual response is to parallelise it rather than to notice that a third of its runtime is one fixture running where it is not needed.
The diagnostic is to time the suite with the fixture temporarily disabled. Adding the opt-out marker to the whole unit subtree through a pytestmark in its conftest.py, or commenting out autouse=True for one run, gives a before-and-after figure in a minute. If the unit suite drops from forty seconds to twenty-five, the fixture belongs in the integration subtree, and moving it is a two-line change that gives every developer those fifteen seconds back on every run for the life of the project.
--durations spreads it too thinly to notice.Auditing an existing suite
Large suites accumulate autouse fixtures the way codebases accumulate global variables — one at a time, each individually reasonable. A periodic audit finds the ones that no longer earn their invisibility.
# Every autouse fixture in the suite, with its location.
grep -rn "autouse=True" tests/ --include=conftest.py
# What a specific test actually gets, including autouse fixtures.
pytest tests/unit/test_parser.py::test_empty_input --fixtures-per-test -q
For each autouse fixture found, ask three questions. Does every test beneath it genuinely need the behaviour, or only some? Is it cheap enough that every test paying for it is acceptable? Would a reader of a failing test be surprised to learn it was running? A "no", "no" or "yes" respectively means it should become an explicit fixture that tests request by name.
The conversion is mechanical: remove autouse=True, and add the fixture name to the parameter list of the tests that need it. The collection-time --fixtures-per-test output before and after confirms nothing else changed, and the tests that now name the fixture document their dependency where a reader will see it.
Good autouse fixtures worth copying
It helps to have concrete examples of fixtures that pass all three audit questions, because they share a shape that is easy to recognise once seen.
The network guard shown above prevents every test from reaching the internet by accident. No test's logic depends on it, every test benefits, and it costs a single attribute patch.
The leak check asserts at teardown that no asyncio tasks, threads or open file descriptors survived the test. It prevents a whole class of order-dependent failures, and the only tests that notice it are the ones that were leaking.
The seed reset sets random.seed and NumPy's generator to a value derived from the test's node id before each test. It makes every test reproducible in isolation, and no test's assertion depends on which seed was chosen.
The environment snapshot records os.environ before each test and restores it after, so a test that sets a variable cannot leak it into the next. monkeypatch.setenv already does this for variables set through it; the snapshot catches the ones set directly.
All four are guards: they prevent something rather than provide something, they are cheap, and a reader of any test in the suite can safely ignore them. That is the profile of an autouse fixture that will still be a good idea in three years, and anything that does not fit it is better off as a fixture tests request by name.
Frequently Asked Questions
When is an autouse fixture the right choice? When the behaviour is genuinely universal and invisible to test logic: blocking network access, resetting a global registry, freezing a random seed, asserting no tasks leaked. If a test could reasonably want the opposite behaviour, it should be an explicit fixture the test requests.
How do I find which autouse fixtures apply to a test?
Run pytest --setup-show on that test, or pytest --fixtures-per-test, which lists every fixture used by each test including autouse ones and where they are defined. Autouse fixtures in a parent conftest.py apply to every test beneath it.
Can a test opt out of an autouse fixture?
Not directly. The fixture can check for a marker on request.node and skip its behaviour, which is the standard pattern for opting out. If many tests need to opt out, the fixture should not be autouse.
Related
- Mastering pytest Fixtures — scope and dependency rules that apply to autouse fixtures too.
- Managing conftest Hierarchies — choosing the narrowest directory for a fixture.
- Blocking Accidental Network Calls in pytest — the canonical good autouse fixture, in full.
- pytest Markers for Conditional Test Execution — registering the opt-out markers strictly.
← Back to Mastering pytest Fixtures