Introduction
Pytest fixtures have scopes (function, class, module, package, session) that determine when they are created and destroyed. A narrower-scoped fixture cannot request a wider-scoped one in ways that violate teardown order, and fixtures with teardown logic (yield fixtures) may not execute cleanup in the expected order when multiple fixtures are involved. The most common errors are ScopeMismatch: You tried to access the function scoped fixture with a session scoped request and fixtures whose teardown runs after dependent resources have already been cleaned up by another fixture's teardown.
Symptoms
ScopeMismatch: You tried to access the 'function' scoped fixture 'db_session' with a 'session' scoped request object, involved factories:
tests/conftest.py:12: def session_fixture(db_session)Or teardown ordering issues:
Error: database connection already closed
# Fixture A closes DB in teardown, but Fixture B tries to clean up data afterCommon Causes
- Scope mismatch: Session-scoped fixture requesting function-scoped fixture
- Teardown order not guaranteed: Multiple yield fixtures may teardown in unexpected order
- Fixture overridden in conftest.py: conftest at different levels creates conflicting fixtures
- Autouse fixtures with side effects: Autouse fixtures run unexpectedly in nested tests
- Fixture not found: Typo in fixture name or fixture defined in wrong conftest.py
- Generator fixture not yielding: Fixture uses return instead of yield, teardown skipped
Step-by-Step Fix
Step 1: Match fixture scopes correctly
```python import pytest
# WRONG: Session fixture cannot request function-scoped fixture # @pytest.fixture(scope='session') # def session_data(db_session): # db_session is function-scoped # return db_session.query(User).all()
# CORRECT: Match scopes or use wider scope for dependencies @pytest.fixture(scope='session') def db_engine(): engine = create_engine('sqlite:///test.db') yield engine engine.dispose()
@pytest.fixture(scope='function') def db_session(db_engine): session = Session(bind=db_engine) yield session session.rollback() session.close() ```
Step 2: Control teardown order with nested fixtures
```python @pytest.fixture(scope='session') def app(): """Create application (tears down last).""" app = create_app() yield app app.shutdown()
@pytest.fixture(scope='function') def client(app): """Create test client (tears down before app).""" with app.test_client() as client: yield client
@pytest.fixture(scope='function', autouse=True) def clean_db(db_session): """Clean database before and after each test (tears down first).""" # Setup: truncate tables db_session.execute(text('TRUNCATE users, orders RESTART IDENTITY CASCADE')) db_session.commit() yield # Teardown: runs before client teardown, which runs before app teardown db_session.rollback() ```
Step 3: Use fixture factories for complex setups
```python @pytest.fixture def user_factory(db_session): """Factory fixture that creates users on demand.""" created_users = []
def _create_user(**kwargs): user = User( name=kwargs.get('name', 'test_user'), email=kwargs.get('email', 'test@example.com'), ) db_session.add(user) db_session.commit() created_users.append(user.id) return user
yield _create_user
# Teardown: clean up all created users for user_id in created_users: db_session.query(User).filter_by(id=user_id).delete() db_session.commit()
# Usage in tests def test_user_workflow(user_factory): user1 = user_factory(name='alice') user2 = user_factory(name='bob') assert user1.name == 'alice' ```
Prevention
- Keep fixture scopes aligned: wider-scoped fixtures should only depend on same-or-wider scopes
- Use yield fixtures for proper teardown instead of addfinalizer
- Document fixture dependencies and scope in conftest.py with docstrings
- Use
pytest --fixturesto inspect available fixtures and their scopes - Avoid autouse fixtures unless the setup is genuinely needed by all tests in scope
- Use fixture factories instead of complex fixture hierarchies when possible
- Run tests with
--setup-showto visualize fixture setup and teardown order