QA How-To
pytest Fixtures Scope Tutorial: function, class, module and session
Follow this pytest Fixtures Scope Tutorial to build and verify function, class, module, and session fixtures with runnable examples and cleanup patterns.
19 min read | 3,048 words
TL;DR
Function, class, module, and session scopes determine a fixture's reuse and teardown boundary. Start narrow for mutable data, share only safe resources, and verify the behavior with pytest --setup-show.
Key Takeaways
- Start with function scope for values a test can mutate.
- Use class scope for shared class context and module scope for a file's read-only data.
- Use session scope for run-wide resources and pair owned resources with teardown.
- A broad fixture can supply a narrow fixture, but not the reverse.
- Parametrized fixtures can set up more than once within their declared scope.
- Run --setup-show to inspect actual setup and teardown boundaries.
pytest Fixtures Scope Tutorial: choose how long a setup value lives before you optimize a test suite. A function fixture is rebuilt for each test, a class fixture is shared by tests in one class, a module fixture is shared within one file, and a session fixture is shared for one pytest invocation. The right scope depends on who may safely share the value, not merely how expensive setup feels.
You will build a tiny suite that proves each lifetime with --setup-show. The examples use ordinary Python objects and temporary files, so they run without a database, browser, or cloud account. After the basic scopes, you will connect fixtures across scopes and check what parametrization does to the cache. If you are new to test discovery and assertions, start with the pytest tutorial for beginners.
TL;DR
| Scope | Created when | Released when | Good default use | Sharing risk |
|---|---|---|---|---|
function |
Each test requests it | That test ends | Mutable test data | Lowest |
class |
First requesting test in a class | Last requesting test in that class ends | Read-only class context | State may leak across methods |
module |
First requesting test in a module | Last requesting test in that module ends | Immutable catalog for one file | State may leak across functions |
session |
First request in a pytest run | The run ends | Expensive shared setup | State may leak across files |
Pytest also supports package scope, between module and session. A fixture executes on first request, not automatically at collection. Use yield when it owns something that must be closed or deleted. A broad fixture may supply a narrow one, but a broad fixture cannot depend on a narrow one. The pytest fixture documentation defines these lifetimes and explains the one-instance cache behavior.
What You Will Build
- A
scope-labproject with an isolated pytest installation and predictable test discovery. - Separate tests that demonstrate fresh function values, shared class values, and shared module values.
- A session fixture backed by
tmp_path_factorythat cleans up its marker after the run. - A three-level dependency chain that keeps mutable per-test data isolated.
- A parametrized module fixture whose two parameters each get their own setup.
All snippets below are complete file contents. Create the named file in the project root unless the path starts with tests/. Keep the earlier files as you progress. Commands use python -m pytest so that the interpreter and installed pytest come from the same environment. A passing result verifies behavior, while --setup-show reveals the setup and teardown boundaries.
Prerequisites
Use Python 3.13 and install pytest into a fresh virtual environment; no third-party pytest plugins are needed. Check your interpreter before installing anything. On macOS or Linux, python3.13 --version should print a 3.13.x version. On Windows, replace python3.13 with py -3.13 in the environment creation command, then use the activated python command as shown. If you are adding these tests to an existing project, follow the pytest version constraint that project already tests in CI.
mkdir scope-lab
cd scope-lab
python3.13 -m venv .venv
source .venv/bin/activate
python -m pip install -U pytest
python --version
python -m pytest --version
On Windows PowerShell, activate with .venv\Scripts\Activate.ps1. If your shell blocks scripts, use .venv\Scripts\python.exe -m pytest without activation. The last command should print the pytest version installed in your environment. Record that version alongside your team's test results so another engineer can reproduce the run. Python's venv documentation covers environment creation.
Step 1: Configure collection before adding fixtures
Create pyproject.toml so running pytest from the project root collects only the example suite. testpaths prevents an unrelated file from entering a later full run, and -ra shows a useful summary if a test skips or fails. There is no application package in this lab, so a minimal pytest configuration is enough.
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra"
Create the test directory. An empty directory is sufficient for collection; pytest does not require tests/__init__.py here. Keep the shell at scope-lab, because later commands and file paths assume this root. If you copy the examples into an existing repository, use its current config rather than adding a second competing pytest configuration.
mkdir tests
python -m pytest --collect-only -q
Verify: pytest should report no tests collected and exit with code 5. That exit code is expected only at this stage. python -m pytest --version should still report the version you installed in Prerequisites. After Step 2, collection must find tests and exit successfully. Avoid using a green-looking command prompt as evidence that empty collection succeeded: the exit status matters in CI.
Step 2: Function isolation in this pytest Fixtures Scope Tutorial
Create tests/test_function_scope.py. Leaving out the scope argument selects function, the default. Each test requests basket by name. The first test mutates its list; the second checks for a fresh list. Neither test depends on which one runs first, because its own assertion describes the fixture value it receives.
# tests/test_function_scope.py
import pytest
@pytest.fixture
def basket():
return ["apple"]
def test_can_add_fruit(basket):
basket.append("pear")
assert basket == ["apple", "pear"]
def test_starts_with_one_fruit(basket):
assert basket == ["apple"]
The key observation is fixture construction twice, not just two passing assertions. --setup-show prints a SETUP F basket line for each test and a matching TEARDOWN F basket after each one. The letter F means function scope. This is a useful diagnostic when a supposedly isolated test unexpectedly sees data from a previous test.
python -m pytest tests/test_function_scope.py -q --setup-show
Verify: expect 2 passed, two SETUP F basket entries, and two TEARDOWN F basket entries. The command is intentionally verbose enough to show the boundary. If you later replace the list with a database row or browser context, keep function scope for mutable state that tests may change. Sharing only the expensive, immutable part can reduce setup cost without coupling outcomes.
Step 3: Share a read-only value within each class
Create tests/test_class_scope.py. A class-scoped fixture is cached separately for each test class that requests it. The two methods in TestNorth see the same tuple; TestSouth gets another fixture instance. The example returns an immutable tuple so there is no cleanup or mutation to coordinate among methods. A class fixture does not require an autouse flag: method arguments request it explicitly.
# tests/test_class_scope.py
import pytest
@pytest.fixture(scope="class")
def region_codes():
return ("US", "CA")
class TestNorth:
def test_us_is_supported(self, region_codes):
assert "US" in region_codes
def test_ca_is_supported(self, region_codes):
assert "CA" in region_codes
class TestSouth:
def test_us_is_still_supported(self, region_codes):
assert region_codes[0] == "US"
Class scope is valuable when several methods exercise one read-only configuration or a fixture whose ownership genuinely belongs to a class. It does not turn the class into a shared-state workflow. In particular, avoid a method that creates a record and a later method that assumes the record exists. Test ordering is not the contract that makes fixtures safe. Give each test its own mutable record even if a class fixture supplies an immutable client or configuration.
python -m pytest tests/test_class_scope.py -q --setup-show
Verify: expect 3 passed. The output should show two SETUP C region_codes lines, one for each class, and teardown after each class's last test. The C denotes class scope. If you see three setup lines, check that the decorator says scope="class" and that the tests actually request the same fixture name.
Step 4: Cache one catalog for a module
Create tests/test_module_scope.py. Here both functions use a small catalog that is built once for this Python module. MappingProxyType makes the dictionary read-only through the returned view. That matters: if a module fixture returned a normal mutable dictionary and one test modified it, the next test in the file would observe the modification. A broader lifetime should usually come with a stricter mutation policy.
# tests/test_module_scope.py
from types import MappingProxyType
import pytest
@pytest.fixture(scope="module")
def service_catalog():
return MappingProxyType({"users": "/api/users", "jobs": "/api/jobs"})
def test_users_route_is_present(service_catalog):
assert service_catalog["users"] == "/api/users"
def test_jobs_route_is_present(service_catalog):
assert service_catalog["jobs"].startswith("/api/")
Module scope means the fixture value is cached for tests in one file. Defining a fixture in a module also limits where pytest can find it: another module will not discover service_catalog merely because it is module-scoped. To share a module-scoped fixture definition across files, put it in tests/conftest.py; pytest then creates an instance for each requesting module. Scope controls lifetime, while definition location controls availability.
python -m pytest tests/test_module_scope.py -q --setup-show
Verify: expect 2 passed, one SETUP M service_catalog, and one TEARDOWN M service_catalog. For a larger suite, try the guide to structuring a test automation repository before collecting every fixture in a single global conftest.py. A fixture placed too high in the directory tree can be visible to tests that should never use it.
Step 5: Session teardown in this pytest Fixtures Scope Tutorial
Create tests/conftest.py and two small test modules. conftest.py shares the fixture definition with tests below its directory. The built-in tmp_path_factory is itself session-scoped, so a session-scoped fixture may depend on it. Use yield to return the workspace to tests, then remove the marker after the last test in this pytest invocation. The temporary directory is managed by pytest and may be retained according to its temporary-directory policy.
# tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def session_workspace(tmp_path_factory):
root = tmp_path_factory.mktemp("scope-lab")
marker = root / "active.txt"
marker.write_text("ready", encoding="utf-8")
yield root
marker.unlink(missing_ok=True)
# tests/test_session_scope_a.py
def test_workspace_a(session_workspace):
assert (session_workspace / "active.txt").read_text(encoding="utf-8") == "ready"
# tests/test_session_scope_b.py
def test_workspace_b(session_workspace):
assert (session_workspace / "active.txt").is_file()
The fixture starts when the first of these tests requests it, not when pytest imports conftest.py. Its teardown runs at session end, including when a test fails after successful setup. If an exception happens before yield, code after yield will not run; acquire resources in small stages or register cleanup as soon as acquisition succeeds. Do not put per-test user data in this shared workspace unless each test gets a unique child path.
python -m pytest tests/test_session_scope_a.py tests/test_session_scope_b.py -q --setup-show
Verify: expect 2 passed, one SETUP S session_workspace, and one TEARDOWN S session_workspace after both tests. The S means session scope. The exact temporary path and order of the two file names are not part of the assertion. This pattern is appropriate for one expensive generated artifact that all tests may read, while tmp_path is preferable for a private directory per test.
Step 6: Compose scopes without leaking writes
Create tests/test_dependency_scopes.py. The module fixture depends on the existing session fixture. The function fixture depends on the module fixture and makes a fresh list for each test. This direction is legal: the broad workspace outlives the module seed, and the module seed outlives each per-test copy. Do not reverse the dependency by asking session_workspace for a function-scoped tmp_path; pytest would raise ScopeMismatch.
# tests/test_dependency_scopes.py
import pytest
@pytest.fixture(scope="module")
def module_seed(session_workspace):
assert (session_workspace / "active.txt").is_file()
return ("draft", "review")
@pytest.fixture
def working_items(module_seed):
return list(module_seed)
def test_can_finish_item(working_items):
working_items[0] = "done"
assert working_items == ["done", "review"]
def test_gets_unmodified_items(working_items):
assert working_items == ["draft", "review"]
This is a practical compromise for API or UI automation: a session-level service can provide stable access, a module-level catalog can hold reusable read-only seed data, and a function-level fixture can create records or lists that a test modifies. The fixture dependency names explain the ownership chain better than hidden setup hooks. If setup is expensive, measure which step costs time before widening a mutable fixture's scope.
python -m pytest tests/test_dependency_scopes.py -q --setup-show
Verify: expect 2 passed, one setup for session_workspace, one for module_seed, and two for working_items. The teardown sequence runs in reverse dependency order. You can inspect a larger real-world example in Playwright Python fixtures with pytest, where the same isolation question applies to browser resources and per-test state.
Step 7: Observe parametrized fixture cache behavior
Create tests/test_parametrized_scope.py. The fixture has module scope, yet it is configured for two parameters. Pytest runs the requesting tests for each backend value. Do not interpret "module scope" as "the fixture function always executes exactly once per file": distinct parameter values require distinct instances. Pytest normally caches one fixture instance at a time, so setup and teardown can occur between parameter groups.
# tests/test_parametrized_scope.py
import pytest
@pytest.fixture(scope="module", params=["memory", "disk"])
def backend(request):
value = request.param
yield value
def test_backend_is_known(backend):
assert backend in {"memory", "disk"}
def test_backend_has_a_name(backend):
assert len(backend) >= 4
request.param is supplied by pytest for a parametrized fixture. The yield is valid even without cleanup after it; it lets --setup-show expose the teardown boundary and leaves a clear place to close a real backend later. In production, close a real connection after yield, and make sure the fixture does not leave one parameter's rows or files for the next. Test IDs include the parameter name, which helps you identify the failing backend.
python -m pytest tests/test_parametrized_scope.py -q --setup-show
python -m pytest -q
Verify: the focused command should report 4 passed, with one SETUP M backend['memory'] and one SETUP M backend['disk'] in the setup trace. The full suite should report 15 passed: 2 function, 3 class, 2 module, 2 session, 2 dependency, and 4 parametrized tests. If the count differs, run python -m pytest --collect-only -q and compare discovered test names before debugging fixture lifetime.
Troubleshooting
Problem: ScopeMismatch appears during setup. -> Read the dependency chain named in the error. A session or module fixture cannot request tmp_path, which has function scope. For shared temporary data, use the session-scoped tmp_path_factory as in Step 5; for private test data, move the dependent fixture to function scope. Widening tmp_path itself is not an option because built-in fixture scopes are fixed.
Problem: a shared fixture appears to run twice. -> First check whether the fixture is parametrized, as in Step 7. Each parameter is a different setup. Also check whether you launched two separate python -m pytest processes; session scope lasts for one process invocation, not across independent runs. With parallel workers, each worker has its own session fixture instance.
Problem: one test passes alone but fails in the full file. -> Look for mutation of a class, module, or session value. Run the file with --setup-show and inspect the shared instance boundary. Make the shared value immutable, copy it in a function fixture, or reset external records before each test. Do not fix a state leak by imposing a test order.
Problem: fixture 'name' not found appears. -> Confirm the spelling in the test argument and fixture function. A fixture inside one test module is not automatically visible in another. Put a shared definition in the nearest suitable conftest.py, then run python -m pytest --fixtures tests to inspect what pytest can discover.
Problem: cleanup never runs after setup raises. -> In a yield fixture, code after yield only runs once execution reaches yield. If resource acquisition can fail halfway through, use try/finally around the relevant step or register a finalizer immediately after acquiring the resource. Keep each fixture responsible for one resource when possible, so partial setup is easier to unwind.
Problem: the suite is slower after choosing session scope. -> Check whether the setup is actually the bottleneck and whether tests now contend for one shared service. Session scope reduces repeated setup but does not make shared mutable state safe or speed up I/O contention. Profile fixture construction and test calls separately before changing ownership. For parallel execution details, see Playwright Python parallel tests with pytest-xdist.
Interview Questions and Answers
Q: What does fixture scope control? Scope controls when pytest creates and discards a fixture instance for a requesting test. It does not control where the fixture name is visible. A definition in conftest.py can be shared across files while still producing a separate value per function, class, or module.
Q: Why is function scope the default? It gives each test a fresh value, which makes mutation less likely to couple tests. I start there for records, request payloads, and writable directories. I widen the scope only when the shared object has a clear ownership boundary and a measured setup cost.
Q: Can a session fixture use tmp_path? No. tmp_path is function-scoped and a session fixture cannot depend on a narrower fixture. tmp_path_factory has session scope and can create a shared directory. A function fixture can then create its own child files when each test needs isolation.
Q: How do you verify fixture lifetime? Run a focused file with python -m pytest --setup-show -q and count setup and teardown entries. The scope letters show the lifetime, while the test results show that assertions still pass. I also run each suspect test alone and within its module to detect leaked state.
Q: What changes when a fixture is parametrized? Pytest produces test cases for each parameter value and may build more than one instance within a declared scope. Module scope therefore does not guarantee one call for the whole file. The parameter ID in the test name helps map failures back to the fixture variant.
Q: When would you choose class scope over module scope? Choose class scope when the resource belongs to a coherent class of related tests and should be torn down before the next class starts. Module scope fits a whole file's read-only catalog or configuration. I would avoid both for mutable data that individual tests update.
Common Mistakes
- Widening scope to hide expensive setup. First split an expensive immutable resource from cheap mutable test data. Sharing a database client can be safe; sharing a row that tests edit usually is not.
- Confusing visibility with lifetime. Moving a fixture to
conftest.pymakes its name available under that directory, but its declared scope still determines reuse. - Depending on test order. A test must be able to request the setup it needs directly. A class fixture does not make one method's side effects a legitimate precondition for another method.
- Ignoring teardown on failure. Use
yieldfor owned resources and verify cleanup under a deliberately failing test in a disposable environment when the resource is costly. - Assuming one session means one CI job. Separate pytest commands and parallel workers have separate fixture caches. Plan external resource names so those runs do not collide.
Where To Go Next
The completed lab gives you a choice rule: keep writable state at function scope; share read-only or carefully partitioned resources only for as long as their owners need them. Use --setup-show during reviews when a scope change is proposed, and record what the trace says before claiming a performance improvement. If your suite calls APIs, the Python API automation framework guide shows where fixture-owned clients and request data fit. For behavior-driven suites, the pytest-bdd tutorial shows how pytest fixtures supply scenario context. The pytest versus unittest guide helps explain fixture composition to teams moving from class setup hooks.
For interview preparation, practice describing one resource you would share and one you would recreate per test. The top pytest interview questions provide more prompts, but use your own fixture trace as evidence. When you add browser automation, revisit Playwright Python fixtures with pytest and keep page or context state isolated according to what each test changes.
Conclusion
A useful pytest fixture scope tutorial ends with observable behavior. You now have examples for function, class, module, and session lifetimes, a safe dependency chain, and a parametrized case that breaks the simplistic "one setup per scope" assumption. Run the full suite, inspect its setup trace, then choose the narrowest scope that fits each real resource's ownership and cleanup needs.
Interview Questions and Answers
How would you select a pytest fixture scope for a database client and test rows?
I would consider a broader scope for a client only if it is safe to share and expensive to establish. Rows that tests insert or update should be created per test or isolated by unique identifiers and cleanup. I would verify the fixture trace and run the suite in parallel before accepting the design.
What is the setup order when a function fixture depends on a module fixture that depends on a session fixture?
Pytest sets up the session dependency first, then the module dependency, then the function fixture. Teardown happens in the reverse dependency order as each scope ends. The broad fixtures may be reused for later requesting tests within their respective boundaries.
Why can a session fixture not request tmp_path?
tmp_path is function-scoped, so its lifetime is shorter than that of a session fixture. The session fixture could retain a path whose owning fixture has ended. I would use session-scoped tmp_path_factory to make a shared directory, then use tmp_path in function fixtures for private files.
How do you diagnose order-dependent failures in a pytest module?
I run the failing test alone and then with its neighboring tests, using --setup-show to identify shared fixtures. I inspect writes to module or session values and external systems. The repair is to isolate or reset state, not to enforce a preferred test order.
What does placing a fixture in conftest.py change?
It changes discovery for tests in that directory tree. It does not automatically change the fixture's scope or make its value global across every file. I place definitions near their consumers to keep unrelated tests from depending on them accidentally.
Does class scope share one object among all test classes in a file?
No. A class-scoped fixture is cached for one requesting class at a time. Another class requesting the same fixture receives its own instance. A module-scoped fixture would be the appropriate lifetime for a read-only value used across the file.
What does parametrization do to fixture caching?
Parametrization creates a distinct requested fixture value for each parameter. Pytest keeps only one cached instance of a fixture at a time, so even a broad-scoped fixture can be set up more than once. I inspect parameter IDs and teardown boundaries before estimating setup counts.
Frequently Asked Questions
What is the default scope of a pytest fixture?
The default is function scope. Pytest creates a fresh fixture value for each requesting test and tears it down after that test finishes. This is the safest starting point for mutable data.
Does session scope mean a fixture runs once across all CI jobs?
No. Session scope covers one pytest invocation. Separate commands or parallel worker processes each create their own session-scoped instances, so external resources need collision-safe names.
Can a module-scoped fixture be defined in conftest.py?
Yes. Placing the definition in a suitable conftest.py makes it discoverable to tests below that directory. Module scope still creates a separate instance for each requesting test module.
When does a yield fixture run its teardown code?
Pytest runs the code after yield when that fixture's scope ends, provided setup reached the yield statement. If setup raises before yield, that fixture's post-yield cleanup is not executed. Already established dependent fixtures can still be finalized.
Why did a module-scoped parametrized fixture run twice?
Each parameter represents a different fixture value. Pytest may create one module-scoped instance for each parameter, so the scope does not imply one total call per module. Inspect --setup-show and the parameterized test IDs.
How do I fix ScopeMismatch in pytest?
Find the broad fixture that requests a narrower dependency. Move the dependent fixture to a narrower scope, or replace the dependency with one safe at the broader scope. For shared temporary files, use tmp_path_factory rather than function-scoped tmp_path.
What is the difference between fixture scope and fixture visibility?
Scope controls how long a created value is reused. Visibility controls which tests can request the fixture name, based on its definition location such as a test module or conftest.py. Changing one does not automatically change the other.
Related Guides
- pytest caplog and capsys Examples: Assert Logs and Output
- pytest Markers Tutorial: skip, xfail and Custom Markers
- API Testing with pytest and requests: Step-by-Step Tutorial
- Appium 3 Config File Setup Tutorial: capabilities and .conf (2026)
- Gatling Feeders Tutorial: CSV, JSON and Custom Feeders
- k6 setup and teardown Tutorial: Share an Auth Token Across VUs