python pytest setup teardown vs fixtures: Which to Use
python pytest setup teardown vs fixtures: Compare xunit-style setup/teardown methods with pytest fixtures to decide which approach improves test isolation, sharing, an...
When writing pytest tests, you have two main ways to prepare and clean up state: the xunit-style setup and teardown methods inherited from unittest, and pytest's fixture system. The choice between python pytest setup teardown vs fixtures affects how tests share state, how they are parametrized, and how maintainable they remain as the suite grows.
The xunit-style setup and teardown methods
Pytest supports xunit-style methods for compatibility with unittest conventions. In a test class, you can define setup_method, teardown_method, setup_class, teardown_class, and similar hooks. These methods run before and after each test or once per class.
class TestDatabase: def setup_method(self): self.db = create_connection() def teardown_method(self): self.db.close() def test_insert(self): self.db.insert("user", 1) assert self.db.count("user") == 1 def test_delete(self): self.db.insert("user", 1) self.db.delete("user", 1) assert self.db.count("user") == 0
The setup_method runs before each test, and teardown_method runs after each test. This ensures each test gets a fresh connection, but it also means the setup logic is repeated for every test in the class. If you need a different setup for a specific test, you must add conditional logic inside the method, which quickly becomes messy.
These methods are also limited to class-based tests. In a function-based test, you would need to use module-level setup_module and teardown_module, which run once for the entire module. That gives you no per-test isolation unless you manually reset state.
How pytest fixtures work
Pytest fixtures are functions that provide a fixed baseline for tests. They are declared with @pytest.fixture and injected into test functions or other fixtures by parameter name. A fixture can return a value, and it can perform cleanup after the test by using yield instead of return.
import pytest @pytest.fixture def db(): connection = create_connection() yield connection connection.close() def test_insert(db): db.insert("user", 1) assert db.count("user") == 1 def test_delete(db): db.insert("user", 1) db.delete("user", 1) assert db.count("user") == 0
The db fixture creates a connection, yields it to the test, and then closes it after the test finishes. This is the idiomatic way to handle teardown in pytest. Fixtures can also depend on other fixtures, which lets you build a composable setup graph.
Comparing setup/teardown methods and fixtures
The practical differences go beyond syntax. The table below summarizes the key distinctions that matter when you choose between python pytest setup teardown vs fixtures.
| Aspect | xunit setup/teardown | pytest fixtures |
|---|---|---|
| Scope | Per method, class, module | Function, class, module, session |
| Sharing across tests | Only within the same class/module | Across any test via conftest.py |
| Parametrization | Not supported directly | Built-in via @pytest.mark.parametrize or fixture params |
| Cleanup | Separate teardown method | yield after the fixture body |
| Dependency injection | Not available; use self | Fixtures can request other fixtures |
| Readability | Setup logic scattered across methods | Setup and cleanup in one place |
| Test isolation | Manual per test | Automatic when fixture is function-scoped |
Fixtures give you explicit control over scope and reuse. A fixture defined in conftest.py can be used by any test in that directory, which is impossible with xunit methods unless you create a base class and inherit it everywhere.
Fixture scoping and sharing
The default fixture scope is function, meaning the fixture is created and torn down for each test. You can change this with the scope parameter: @pytest.fixture(scope="module") creates the fixture once per module, and scope="session" creates it once for the entire test session.
@pytest.fixture(scope="module") def database(): conn = create_connection() yield conn conn.close() def test_one(database): assert database.is_open() def test_two(database): assert database.is_open()
Both tests receive the same database object. This is useful for expensive resources like database connections or HTTP clients. However, if tests mutate the resource, you need to handle resetting state yourself. Function-scoped fixtures avoid this by giving each test a fresh instance.
conftest.py is the standard place to define shared fixtures. Any fixture defined there is automatically available to tests in that directory and subdirectories, without importing anything. This is a major advantage over xunit methods, which require inheritance or module-level helpers.
Parametrization with fixtures
Fixtures support parametrization directly, which is one of the strongest reasons to prefer them over setup/teardown. You can define a fixture with params and each parameter value will cause all tests that use the fixture to run once per value.
@pytest.fixture(params=["sqlite", "postgres"]) def db_connection(request): conn = create_connection(request.param) yield conn conn.close() def test_insert(db_connection): assert db_connection.is_connected()
This runs test_insert twice, once for each database type. The request.param attribute gives you access to the current parameter. To achieve the same with xunit methods, you would need to loop inside the test or use a different mechanism, which obscures the test's intent.
You can also combine parametrized fixtures with @pytest.mark.parametrize on the test function to test combinations of inputs and fixture states. This composability is not available in the xunit model.
When to choose fixtures over setup/teardown
Use fixtures when you need any of the following:
- Sharing setup across multiple test modules without inheritance.
- Parametrizing tests with different resource configurations.
- Composing dependencies, where one fixture depends on another.
- Controlling scope at a fine-grained level (function, class, module, session).
- Keeping cleanup code next to setup code for readability.
Xunit setup/teardown methods are still acceptable for small, class-based tests where you want to keep the unittest-style structure and don't need advanced features. But they become a liability as soon as you need to share state across classes or parametrize tests. The fixture system is the pytest-native way and integrates with the rest of the ecosystem, such as plugins and markers.
Common pitfalls and maintainability
Mixing both styles in the same test suite can lead to confusion. For example, if you have a setup_method that creates a resource and also a fixture that creates the same resource, you may end up with two instances or unexpected ordering. Pytest runs fixtures before setup methods, but relying on that order is fragile. Prefer one style consistently.
Another pitfall is using a session-scoped fixture for mutable state without resetting it between tests. If a test modifies the resource, later tests may see stale data. In that case, either use function scope or explicitly reset the state in the fixture's yield section.
When a fixture fails during setup, the teardown part after yield is not executed. This is correct behavior, but it means you should not place cleanup logic that must run even on setup failure in the fixture body after yield. Instead, use try/finally if you need that guarantee, or rely on the fixture's finalizer mechanism via request.addfinalizer.
Finally, keep fixtures focused. A fixture that does too much—like creating a database, seeding data, and starting a server—becomes hard to reuse and debug. Split it into smaller fixtures that depend on each other. This improves maintainability and makes it easier to override individual pieces in specific tests.