Skip to content
← Back to explainers

Python explainer

Share Setup and Cleanup with Fixtures

About 4 min to read. Allow extra time to work through examples.

Move setup that several tests repeat into a pytest fixture, clean up after every test with yield, and choose how long a fixture's value lives.

This article uses Python. You can make it your practice mode to browse related content across the site.

Two tests for a user table each open a database, create the table and add two users before they check anything. Save this as test_users.py and run pytest:

import sqlite3


def test_count_users() -> None:
    db = sqlite3.connect(":memory:")
    db.execute("CREATE TABLE users (name TEXT)")
    db.executemany("INSERT INTO users VALUES (?)", [("ada",), ("grace",)])
    assert db.execute("SELECT count(*) FROM users").fetchone() == (2,)
    db.close()


def test_find_user() -> None:
    db = sqlite3.connect(":memory:")
    db.execute("CREATE TABLE users (name TEXT)")
    db.executemany("INSERT INTO users VALUES (?)", [("ada",), ("grace",)])
    assert db.execute("SELECT name FROM users").fetchone() == ("ada",)
    db.close()

Both tests repeat the same three lines of setup. And when an assert fails, the test stops there, so db.close() never runs.

Move the setup into a fixture

A fixture is a function that prepares something your tests need. You mark it with @pytest.fixture. A test asks for a fixture by naming it as a parameter, and pytest calls the fixture and passes in the result.

import sqlite3
from collections.abc import Iterator

import pytest


@pytest.fixture
def db() -> Iterator[sqlite3.Connection]:
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (name TEXT)")
    conn.executemany("INSERT INTO users VALUES (?)", [("ada",), ("grace",)])
    yield conn
    conn.close()


def test_count_users(db: sqlite3.Connection) -> None:
    assert db.execute("SELECT count(*) FROM users").fetchone() == (2,)


def test_find_user(db: sqlite3.Connection) -> None:
    assert db.execute("SELECT name FROM users").fetchone() == ("ada",)

pytest runs the fixture up to yield, and the test receives the value it yields. The code after yield is the teardown, the cleanup that runs after the test. pytest runs it whether the test passes or fails. Each test gets a fresh connection of its own.

A fixture with yield is a generator, so its return type is Iterator[sqlite3.Connection].

Let fixtures ask for other fixtures

A fixture can ask for another fixture through its parameters, the same way a test does. pytest sets up the dependency first. After the test, it tears the fixtures down in reverse order. Run this with pytest -s, which shows the prints:

import sqlite3
from collections.abc import Iterator

import pytest


@pytest.fixture
def conn() -> Iterator[sqlite3.Connection]:
    print("open connection")
    conn = sqlite3.connect(":memory:")
    yield conn
    print("close connection")
    conn.close()


@pytest.fixture
def users(conn: sqlite3.Connection) -> Iterator[sqlite3.Connection]:
    print("create table")
    conn.execute("CREATE TABLE users (name TEXT)")
    yield conn
    print("drop table")
    conn.execute("DROP TABLE users")


def test_starts_empty(users: sqlite3.Connection) -> None:
    print("run test")
    assert users.execute("SELECT count(*) FROM users").fetchone() == (0,)
open connection
create table
run test
drop table
close connection

The users teardown drops the table while the connection is still open. pytest makes each fixture once per test, so a test that asks for both conn and users gets one connection.

pytest sets the order from dependencies, scope and autouse. Fixture names and the order of a test's parameters do not count. If one fixture must run before another, make it a dependency.

Give each resource its own fixture

Suppose one fixture opens a database and then starts a worker, and the worker fails to start. The fixture never reaches yield, so its teardown never runs and the database stays open. Split it into a database fixture and a worker fixture that asks for it. When worker fails, pytest still closes the database, because that fixture finished its setup.

Choose a scope

A fixture's scope sets how long pytest keeps its value before it runs the teardown. The default, "function", makes a new value for every test. scope="module" makes one for all the tests in a file, and scope="session" makes one for the whole run. There are also "class" and "package" scopes.

A wider scope saves time when setup is slow, but the tests then share one object. If one test adds a row, the next test sees it, so a test can pass or fail depending on which tests ran first. Give the slow part a wide scope, and reset the shared state in a function-scoped fixture:

import sqlite3
from collections.abc import Iterator

import pytest


@pytest.fixture(scope="module")
def shared_conn() -> Iterator[sqlite3.Connection]:
    conn = sqlite3.connect(":memory:")
    conn.execute("CREATE TABLE users (name TEXT)")
    yield conn
    conn.close()


@pytest.fixture
def db(shared_conn: sqlite3.Connection) -> Iterator[sqlite3.Connection]:
    yield shared_conn
    shared_conn.execute("DELETE FROM users")


def test_add_user(db: sqlite3.Connection) -> None:
    db.execute("INSERT INTO users VALUES ('ada')")
    assert db.execute("SELECT count(*) FROM users").fetchone() == (1,)


def test_starts_empty(db: sqlite3.Connection) -> None:
    assert db.execute("SELECT count(*) FROM users").fetchone() == (0,)

A module-scoped fixture cannot ask for a function-scoped one, because pytest would tear down the function-scoped value after the first test. pytest reports a ScopeMismatch error.

Check your understanding A test fails at its assert. Does the code after yield in its fixture still run? Show answer

Yes. pytest runs the teardown after every test that used the fixture, pass or fail. It skips only the teardown of a fixture that raised before it reached yield. That is why each resource gets its own fixture, so that pytest can still tear down the ones that finished their setup.

Return a function when tests need different data

A factory fixture returns a function that builds what the test needs. Each test calls it as often as it likes, with its own data, and the fixture cleans up everything it made:

import sqlite3
from collections.abc import Callable, Iterator

import pytest


@pytest.fixture
def make_db() -> Iterator[Callable[[list[str]], sqlite3.Connection]]:
    opened: list[sqlite3.Connection] = []

    def make(names: list[str]) -> sqlite3.Connection:
        conn = sqlite3.connect(":memory:")
        conn.execute("CREATE TABLE users (name TEXT)")
        conn.executemany("INSERT INTO users VALUES (?)", [(n,) for n in names])
        opened.append(conn)
        return conn

    yield make
    for conn in opened:
        conn.close()


def test_databases_are_separate(
    make_db: Callable[[list[str]], sqlite3.Connection],
) -> None:
    empty = make_db([])
    full = make_db(["ada", "grace"])
    assert empty.execute("SELECT count(*) FROM users").fetchone() == (0,)
    assert full.execute("SELECT count(*) FROM users").fetchone() == (2,)

Keep autouse fixtures few

@pytest.fixture(autouse=True) runs a fixture for every test in its file without the tests asking for it. Use it for setup that every test needs, such as clearing a module-level cache. Keep such fixtures few, because a test's parameters no longer show all of its setup.

The next exercise has you work out fixture setup and teardown order yourself.

Going further

request.addfinalizer(fn) registers a cleanup function from inside a fixture. If you register several, pytest runs them in reverse order. contextlib.ExitStack does the same job in a yield fixture. pytest's guide How to use fixtures covers each feature, and the fixtures reference lists the ordering rules under "Fixture instantiation order".

step 3 of 19

Testing with pytest and Hypothesis

Up next Work Out Fixture Setup and Teardown Order