Fixtures are pytest's killer feature. They replace the clunky setUp and tearDown methods from unittest with something far more elegant: dependency injection for tests.

What Are Fixtures?

A fixture is a function that provides data or resources to your tests. Instead of creating test data inside each test, you declare what you need as function parameters:

import pytest
 
@pytest.fixture
def user():
    return {"name": "Alice", "email": "alice@example.com"}
 
def test_user_has_name(user):
    assert user["name"] == "Alice"
 
def test_user_has_email(user):
    assert "email" in user

Pytest sees that test_user_has_name takes a user parameter, finds the matching fixture, runs it, and passes the result to your test. Clean and explicit.

Fixture Scope

By default, fixtures run once per test function. But you can control this with the scope parameter:

@pytest.fixture(scope="function")  # Default: runs for each test
def fresh_list():
    return []
 
@pytest.fixture(scope="class")  # Runs once per test class
def class_resource():
    return SomeExpensiveObject()
 
@pytest.fixture(scope="module")  # Runs once per module
def db_connection():
    conn = create_connection()
    yield conn
    conn.close()
 
@pytest.fixture(scope="session")  # Runs once for entire test session
def app_config():
    return load_config()

Use broader scopes for expensive resources like database connections or external services. Use function scope when tests need fresh, isolated data.

Setup and Teardown with yield

The yield statement is your friend for cleanup:

@pytest.fixture
def temp_file():
    # Setup
    path = Path("/tmp/test_file.txt")
    path.write_text("test data")
    
    yield path  # This is what the test receives
    
    # Teardown (runs after test completes)
    path.unlink()
 
def test_read_file(temp_file):
    assert temp_file.read_text() == "test data"
    # File is automatically cleaned up after this test

Everything before yield is setup. Everything after is teardown. The teardown runs even if the test fails.

conftest.py: Sharing Fixtures

When multiple test files need the same fixtures, put them in conftest.py:

tests/
├── conftest.py          # Fixtures available to all tests
├── test_users.py
├── test_orders.py
└── api/
    ├── conftest.py      # Fixtures for api tests only
    └── test_endpoints.py
# tests/conftest.py
import pytest
 
@pytest.fixture
def api_client():
    from myapp import create_app
    app = create_app(testing=True)
    return app.test_client()
 
@pytest.fixture
def authenticated_client(api_client):
    api_client.post("/login", json={"user": "test", "pass": "test"})
    return api_client

Pytest automatically discovers conftest.py files. No imports needed—fixtures just work.

Fixtures Using Other Fixtures

Fixtures can depend on other fixtures:

@pytest.fixture
def database():
    db = Database(":memory:")
    db.create_tables()
    yield db
    db.close()
 
@pytest.fixture
def user_repo(database):
    return UserRepository(database)
 
@pytest.fixture
def sample_user(user_repo):
    user = User(name="Test User", email="test@example.com")
    user_repo.save(user)
    return user
 
def test_find_user(user_repo, sample_user):
    found = user_repo.find_by_email("test@example.com")
    assert found.name == "Test User"

Pytest resolves the dependency chain automatically. When test_find_user runs, it gets both fixtures, properly initialized in order.

Parameterized Fixtures

Fixtures can generate multiple values, running tests for each:

@pytest.fixture(params=["sqlite", "postgres", "mysql"])
def database_type(request):
    return request.param
 
def test_connection(database_type):
    # This test runs three times, once for each database type
    assert database_type in ["sqlite", "postgres", "mysql"]

The request Object

The special request fixture gives you information about the test being run:

@pytest.fixture
def resource(request):
    name = request.node.name  # Current test name
    print(f"Setting up for {name}")
    
    resource = create_resource()
    
    def cleanup():
        print(f"Tearing down {name}")
        resource.close()
    
    request.addfinalizer(cleanup)
    return resource

autouse: Fixtures That Always Run

Sometimes you want a fixture to run for every test without explicitly requesting it:

@pytest.fixture(autouse=True)
def reset_environment():
    os.environ["MODE"] = "test"
    yield
    os.environ.pop("MODE", None)

Use sparingly. Explicit is usually better than implicit.

Real-World Example

Here's a practical setup for testing a Flask app:

# conftest.py
import pytest
from myapp import create_app, db
 
@pytest.fixture(scope="session")
def app():
    app = create_app({"TESTING": True, "DATABASE_URL": "sqlite://"})
    with app.app_context():
        db.create_all()
    yield app
 
@pytest.fixture
def client(app):
    return app.test_client()
 
@pytest.fixture
def runner(app):
    return app.test_cli_runner()
 
@pytest.fixture
def auth_headers(client):
    response = client.post("/auth/login", json={
        "username": "testuser",
        "password": "testpass"
    })
    token = response.json["token"]
    return {"Authorization": f"Bearer {token}"}

Fixtures transform messy test setup into clean, composable building blocks. Start using them, and you'll never go back to the old way.

React to this post: