šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

pytest Fixtures

pytest fixtures — dependency injection for tests. Reusable setup/teardown, scoping, and why fixtures beat copy-pasted setup code in every test function.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

How does test_user_has_valid_email(sample_user) receive the fixture's value?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Most tests need something set up before they run — a database connection, a temp file, a configured object. Fixtures are pytest's answer to sharing that setup cleanly across many tests, without copy-pasting the same setup code (and forgetting the same cleanup) in every single test function.

1Fixtures as Dependency Injection for Tests

Without fixtures, any setup a test needs — constructing a sample object, establishing a test database connection — either gets copy-pasted at the top of every test function that needs it (real duplication, with all the maintenance risk that implies: change the setup logic, and now ten separate copies need updating consistently), or gets extracted into a plain helper function that every test explicitly calls, which works but doesn't provide any additional structure beyond ordinary function calls.

@pytest.fixture turns a function into something closer to dependency injection: sample_user, decorated as a fixture, becomes available to any test simply by naming a parameter with that exact same name — def test_user_has_valid_email(sample_user): — and pytest automatically calls the fixture function and passes its return value in, with no explicit import or manual invocation needed inside the test body itself. Multiple tests requesting the same fixture name all share the identical setup logic, defined exactly once.

This is a genuinely different mechanism than a plain helper function, even though the code inside a simple fixture might look similar — fixtures participate in pytest's dependency graph (fixtures can themselves depend on other fixtures, simply by naming them as parameters), support the scoping and yield-based teardown covered next, and are automatically discoverable from a shared conftest.py file across an entire test directory without needing to be explicitly imported into every test file that uses them.

āœ•
—
+
import pytest

@pytest.fixture
def sample_user():
    return {"id": 1, "name": "Ada", "email": "ada@example.com"}

def test_user_has_valid_email(sample_user):
    assert "@" in sample_user["email"]

def test_user_has_name(sample_user):
    assert sample_user["name"] == "Ada"
# Both tests share the SAME fixture definition -- change it once, both update
localhost:3000
Automatic Injection
def test_x(sample_user):
pytest matches the parameter name, calls the fixture, injects the result

2yield: Splitting Setup From Guaranteed Teardown

A fixture using return (like sample_user) provides setup only — the value is constructed and handed to the test, with nothing further to clean up afterward, appropriate for simple, stateless data. Many real fixtures need genuine teardown — closing a database connection, deleting a temporary file, releasing a lock — that must run after the test completes, *regardless* of whether that test passed or failed.

yield inside a fixture function splits it into exactly these two phases: everything before yield is setup, executed before the test runs; the value passed to yield is what the test receives as its fixture parameter; and everything *after* yield is teardown, executed once the test has finished — critically, pytest guarantees this teardown code runs whether the test passed, failed with an assertion error, or raised an unexpected exception, mirroring exactly the guarantee a context manager's __exit__ or a try/finally block provides, covered in depth in the Context Managers lesson.

This guarantee matters concretely: without it, a failing test could leave a test database connection open, a temporary file undeleted, or a lock unreleased — resource leaks that can cause subsequent, unrelated tests to fail mysteriously (a classic, confusing source of flaky test suites) simply because an earlier test's cleanup never ran. yield-based fixtures make that class of bug structurally impossible for any fixture written this way.

āœ•
—
+
import pytest

@pytest.fixture
def temp_database():
    db = create_test_database()   # SETUP -- runs before the test
    yield db                       # the test receives 'db' here
    db.close()                     # TEARDOWN -- runs after the test, even if it FAILED
localhost:3000
Guaranteed Cleanup
yield db
Code after yield (db.close()) runs regardless of test outcome

3Scope: Trading Isolation for Speed, Deliberately

By default, a fixture's scope is "function" — its setup code runs fresh, once, for *every single test* that requests it, guaranteeing each test gets a completely clean, isolated instance with no state leaking between tests. This is the safest default and the right choice for anything where cross-test contamination would be a real correctness concern — a mutable data structure, a database transaction that should roll back between tests.

Some setup is genuinely expensive and doesn't need to be repeated per-test to maintain correctness — loading a large machine learning model, spinning up a genuinely heavyweight test container. scope="session" tells pytest to run that fixture's setup exactly *once*, for the entire test run, sharing the identical instance across every test that requests it — a substantial speed improvement for expensive setup, at the deliberate cost of tests now sharing state (which is safe specifically when that state is read-only or otherwise cannot be mutated by any test in a way that would affect others).

Other scope values ("class", "module") sit between these two extremes, sharing setup across a narrower group of tests than the whole session but a broader group than a single test function. Choosing scope deliberately — matching it to how expensive the setup genuinely is and how safe sharing state actually is for that specific fixture — is a real engineering trade-off, not a default to set-and-forget; the wrong scope choice can either waste significant time re-running expensive setup unnecessarily, or introduce subtle, hard-to-diagnose test interdependencies from inappropriately shared state.

āœ•
—
+
@pytest.fixture(scope="function")   # default: fresh instance per test
def fresh_counter():
    return {"count": 0}

@pytest.fixture(scope="session")     # created ONCE, shared across every test
def expensive_ml_model():
    return load_large_model()  # loaded ONCE, not once per test -- much faster
localhost:3000
Scoped Setup
scope="session"
Expensive setup runs ONCE, shared across every test — a deliberate trade-off

4Step-by-Step Breakdown

Copy-pasting the same setup code into ten test functions means ten places to update when that setup logic needs to change. Fixtures fix that with something close to dependency injection for tests.

A fixture is a function decorated with @pytest.fixture -- a test that names it as a PARAMETER automatically receives its return value.

Checkpoint: How does test_user_has_valid_email(sample_user) receive the fixture's value?

  • →pytest matches the parameter name "sample_user" to the fixture function of the same name and calls it automatically
  • →The test must explicitly import and call sample_user() itself

yield (instead of return) in a fixture separates SETUP from TEARDOWN -- code after yield runs automatically after the test finishes.

Checkpoint: Does the code after yield in a fixture (db.close()) run if the test itself FAILS with an assertion error?

  • →Yes — teardown code after yield runs regardless of whether the test passed or failed
  • →No — it only runs if the test passes successfully

Fixture SCOPE controls how often setup runs -- 'function' (default, every test) vs 'session' (once for the ENTIRE test run) trades isolation for speed.

Fixtures handle real setup and teardown; Mocking is the next tool, for isolating a test from its slow or unpredictable real dependencies entirely.

Guarantee Real Fixture Teardown. Finish with_fixture(): a fixture's teardown always runs, even if the test fails.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Use yield-based fixtures whenever genuine teardown (closing, deleting, releasing) is needed

This guarantees cleanup runs regardless of whether the test passes or fails, preventing resource leaks that cause confusing, flaky failures in unrelated later tests.

Default to function scope; widen to session/module scope deliberately, only for genuinely expensive, safely-shareable setup

Function scope guarantees test isolation by default. Widening scope is a real trade-off worth making consciously for expensive setup, not a default optimization to apply everywhere.

Frequent Bugs

THE BUG

Using return instead of yield in a fixture that needs teardown (like closing a database connection), causing that cleanup code to never run at all, since there's no mechanism for code after a return statement to execute.

THE FIX

Use yield instead of return whenever a fixture needs teardown logic, placing the cleanup code after the yield statement so pytest runs it automatically once the test completes.

Real-World Examples

A Database Fixture With Guaranteed Rollback

A test suite needs each test to run against a real test database, but every test should see a clean, isolated database state, with any changes rolled back afterward regardless of whether the test passes.

import pytest

@pytest.fixture
def db_session():
    connection = test_engine.connect()
    transaction = connection.begin()
    session = Session(bind=connection)
    yield session
    session.close()
    transaction.rollback()  # always runs -- undoes any changes the test made
    connection.close()

def test_create_user(db_session):
    user = User(name="Ada")
    db_session.add(user)
    db_session.commit()
    assert db_session.query(User).count() == 1

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Defining a fixture with return instead of yield when it needs teardown logic, causing that cleanup code to be unreachable and never execute.

# Wrong: cleanup code after return is unreachable, never runs @pytest.fixture def temp_database(): db = create_test_database() return db db.close() # DEAD CODE -- never executes # Correct: teardown guaranteed to run @pytest.fixture def temp_database(): db = create_test_database() yield db db.close()

The Solution //

Use yield in place of return whenever a fixture needs to run cleanup code after the test completes, placing that cleanup after the yield statement.

Lesson Glossary

[01]@pytest.fixture

A decorator marking a function as a fixture, whose return (or yielded) value is automatically injected into any test naming it as a parameter.

Code Preview
// @pytest.fixture context

[02]yield fixture

A fixture using yield instead of return, splitting the function into guaranteed setup (before yield) and teardown (after yield) phases.

Code Preview
// yield fixture context

[03]Fixture scope

A fixture parameter controlling how often its setup runs — "function" (default, per test), "class", "module", or "session" (once total).

Code Preview
// Fixture scope context

[04]conftest.py

A special pytest file for sharing fixtures across multiple test files in a directory without explicit imports.

Code Preview
// conftest.py context

Continue Learning