Pytest Yield Fixtures: Reliable Teardown That Runs Even When Tests Fail
Pytest yield fixtures guarantee teardown runs after every test — pass, fail, or error. Learn the pattern, scope behavior, verification, and autouse pitfalls.
25 Jun 2026, 08:53 UTC

The problem: cleanup that never runs
Tests that open files, start subprocesses, seed a database, or create temp directories all share the same risk: if the test body raises, naive cleanup code never executes. In pytest, the supported answer is the yield fixture: you write setup before yield, hand the resource to the test at the yield line, and put teardown after it. Pytest guarantees the post-yield code runs after the test finishes — pass, fail, or error — so resources are released deterministically instead of leaking across the test session.
This guide shows how to convert a setup/teardown fixture to the yield style, how scope changes when teardown runs, and how to verify teardown actually executed.
Prerequisites
- pytest installed (the yield-fixture style has been supported since pytest 3.0; examples assume pytest 7+ / Python 3.9+).
- A test file, e.g.
test_resources.py, runnable withpytestfrom your project root. - No special permissions needed; examples use only the standard library.
Converting a fixture to yield style
Suppose a test needs a temporary config file that must be deleted afterward. The yield fixture looks like this:
import os
import pytest
@pytest.fixture
def config_file(tmp_path):
path = tmp_path / "app.conf"
path.write_text("mode=debug\n")
yield str(path) # test receives this value
# teardown: runs even if the test raised
if path.exists():
path.unlink()
def test_reads_config(config_file):
with open(config_file) as f:
assert "debug" in f.read()
def test_fails_but_cleanup_runs(config_file):
assert os.path.exists(config_file)
assert False # teardown still executes after this failureRun with pytest test_resources.py -v. The second test fails, but the teardown after yield still runs — pytest resumes the generator after the test completes, regardless of outcome.
How scope changes when teardown fires
The scope argument controls the fixture's lifecycle:
| Scope | Setup runs | Teardown runs |
|---|---|---|
| function (default) | before each test | after each test |
| module | before first test in file | after last test in file |
| session | once per pytest run | at end of the run |
Wider scopes amortize expensive setup (e.g., a database container) but delay teardown. Be careful: if a session-scoped fixture's teardown depends on state a function-scoped fixture cleans up first, ordering can bite you. Pytest finalizes fixtures in reverse order of setup, but nested cross-scope dependencies can still produce surprises — keep teardown self-contained where possible.
Verifying teardown actually ran
Don't take the guarantee on faith — verify it once in your own suite. A simple check uses a sentinel file:
import os
import pytest
SENTINEL = "teardown_ran.txt"
@pytest.fixture
def resource(tmp_path):
marker = tmp_path / SENTINEL
yield tmp_path
marker.write_text("done")
def test_uses_resource(resource):
assert resource.exists()
def test_teardown_evidence(tmp_path_factory):
# after the session, confirm marker exists for the fixture's tmp dir
pass # see manual check belowPractical checks:
- Run
pytest test_resources.py --tb=shortwith a deliberately failing test and add aprint("teardown")after the yield; with-s(no capture) you'll see the print after the failure traceback. - For a stronger assertion, have teardown append to a module-level list and assert on it in a later test or a session-scoped finalizer.
autouse: implicit cleanup, implicit risk
Adding autouse=True applies the fixture to every test in scope without naming it:
@pytest.fixture(autouse=True)
def reset_state():
global_cache.clear()
yield
global_cache.clear()This is convenient for stateless guarantees, but two cautions apply. First, autouse teardown runs for tests that may not need it, adding noise to failure output. Second, if teardown itself raises, it can mask the original test failure — pytest reports the teardown error, and the real cause gets harder to find. Keep autouse finalizers trivial and exception-safe (wrap risky cleanup in try/finally or suppress secondary errors deliberately).
Limitations and edge cases
- Only one
yieldper fixture — yielding twice raises an error. Usecontextlib.ExitStackif you need to compose multiple resources. - If teardown code raises, pytest reports it as an error in the teardown phase, which can obscure the test result. Make teardown idempotent and defensive.
- Session-scoped teardown runs at interpreter shutdown-adjacent time; don't rely on it for anything the OS must clean (prefer OS-level primitives like
tmp_path, which pytest manages). - Fixtures using
request.addfinalizerare the older style and still work, but yield fixtures are the recommended, more readable approach.
Recovery when teardown is skipped
Teardown after yield is guaranteed for normal test outcomes, but a hard process kill (SIGKILL, OOM killer, pytest -x combined with a crash in C extension code) skips it. Mitigations:
- Prefer resources the OS reclaims automatically: temp files under
tmp_path, sockets, subprocesses withfinally-guardedterminate(). - For external resources (cloud instances, database rows), tag them with a run ID and run a periodic janitor script rather than trusting test teardown alone.
The takeaway: yield fixtures turn cleanup from a best-effort afterthought into a structural guarantee. Write teardown to be idempotent, verify it once with a failing test, and reserve autouse for genuinely universal cleanup.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.