Tutorial

Testing Python Code with pytest Fixtures

Learn how to use pytest fixtures to eliminate repetitive setup code, manage cleanup automatically, and write cleaner, more reliable tests. This tutorial covers fixture scopes, dependencies, parametrization, and real-world patterns.

July 2026 8 min read 13 views 0 hearts

Have you ever written tests that felt more like a chore than a helpful safety net? You're not alone. Many Python developers start writing tests but quickly get bogged down with repetitive setup code, like creating database connections or loading test data. That's where pytest fixtures come to the rescue.

What Makes Fixtures Special

Think of fixtures as smart assistants that prepare your testing environment. Instead of writing the same setup code in every test function, you define it once and reuse it everywhere. But here's the real magic, fixtures in pytest automatically handle cleanup too.

Let me show you a common problem. Without fixtures, your tests might look like this:

def test_user_creation():
    db = DatabaseConnection()
    result = db.create_user("alice@example.com")
    assert result.success
    db.close()

def test_user_deletion():
    db = DatabaseConnection()
    user = db.create_user("bob@example.com")
    result = db.delete_user(user.id)
    assert result.success
    db.close()

See how we're repeating the same connection logic? Now let's transform this with a fixture:

import pytest

@pytest.fixture
def db():
    connection = DatabaseConnection()
    yield connection
    connection.close()

def test_user_creation(db):
    result = db.create_user("alice@example.com")
    assert result.success

def test_user_deletion(db):
    user = db.create_user("bob@example.com")
    result = db.delete_user(user.id)
    assert result.success

The yield statement does something clever here. Everything before it runs as setup, and everything after runs as teardown. This means your database connection gets properly closed after each test, keeping your test suite clean and reliable.

Working with Fixture Scopes

Sometimes you need a fixture that only runs once for an entire test file, or maybe even once for the whole test suite. Pytest gives you five options for fixture scope:

  • function (default) - Runs before each test function
  • class - Runs once per test class
  • module - Runs once per test module
  • package - Runs once per test package
  • session - Runs once for the entire test session

Imagine you're testing against a slow API. You wouldn't want to authenticate with every single test. Here's how to set that up:

@pytest.fixture(scope="session")
def api_token():
    response = requests.post("https://api.pythonskillset.com/auth", 
                           json={"key": "test-key"})
    return response.json()["token"]

@pytest.fixture
def api_client(api_token):
    return ApiClient(api_token)

Now the authentication happens just once for all your tests, while each test gets its own fresh API client.

When Fixtures Need Each Other

This is where pytest really shines. Fixtures can depend on other fixtures, and pytest figures out the order automatically. Let's say you're testing a blog application:

@pytest.fixture
def database():
    db = create_test_database()
    yield db
    drop_test_database(db)

@pytest.fixture
def sample_author(database):
    author = database.add_author("Jane Doe")
    return author

@pytest.fixture
def sample_post(sample_author, database):
    post = database.add_post(
        title="Python Tips",
        content="Content here...",
        author_id=sample_author.id
    )
    return post

def test_post_has_author(sample_post, sample_author):
    assert sample_post.author_id == sample_author.id

Notice how sample_post automatically uses sample_author. Pytest builds a dependency graph and runs everything in the right order. This makes your tests read like a story instead of a technical recipe.

Parametrizing Fixtures for Multiple Scenarios

Sometimes you want the same fixture to produce different values for different tests. Pytest handles this beautifully:

@pytest.fixture(params=[1, 10, 100])
def user_count(request):
    return request.param

def test_create_users(user_count):
    users = [User() for _ in range(user_count)]
    assert len(users) == user_count

This runs the test three times with different user counts. And the best part, you can combine this with other fixtures to create powerful test combinations.

Practical Tips from Real Projects

At PythonSkillset, we've learned some valuable lessons while building our testing infrastructure. One pattern that saved us countless hours is the factory fixture approach:

@pytest.fixture
def make_user(database):
    created_users = []

    def _make_user(email=None):
        user = User(email or f"user{len(created_users)}@test.com")
        database.add(user)
        created_users.append(user)
        return user

    yield _make_user

    for user in created_users:
        database.delete(user)

This lets you create users on demand within your tests, while still ensuring everything gets cleaned up:

def test_user_registration(make_user):
    user = make_user("new@test.com")
    assert user.active

def test_multiple_users(make_user):
    alice = make_user("alice@test.com")
    bob = make_user("bob@test.com")
    assert alice.id != bob.id

Common Pitfalls and How to Avoid Them

I've seen teams struggle with fixtures that mutate shared state between tests. Always remember, if you're using scope="session" with mutable objects, you're asking for trouble. Here's what happens:

session_items = []

@pytest.fixture(scope="session")
def shared_list():
    return session_items

def test_add_item(shared_list):
    shared_list.append("test")
    assert len(shared_list) == 1

def test_another_item(shared_list):
    shared_list.append("another")
    # This test fails! Length is now 2, not 1

The solution is simple, unless you're certain about thread safety and test isolation, stick with the default function scope. Save session fixtures for read-only data like configuration or API endpoints.

Writing Tests That Make You Sleep Better

Good fixtures transform testing from a burden into a peace of mind. When you set up your fixtures right, you can write tests that are both readable and reliable. Start with simple fixtures for your database connections or API clients, then gradually introduce more complex patterns as your project grows.

The real test of good fixtures is this: can another developer look at your test file and understand what's being tested without reading through pages of setup code? If yes, you're on the right track.

Now go write some tests. Your future self will thank you at 2 AM when you need to debug a production issue.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.