Skip to content

Pytest session-scoped fixture is re-initialized unexpectedly #13755

@sgonorov

Description

@sgonorov

Description

There appears to be an issue with pytest's test execution order that causes a session-scoped, parametrized fixture to be set up and torn down multiple times with the same parameters within a single test session. This behavior is contrary to the expectation that a session-scoped fixture instance should be created only once per unique parameter set and reused for all tests that require it.

Steps to Reproduce

The issue can be observed with the provided test file. The heavy_fixture is session-scoped and parametrized.

import logging
import pytest
import collections

logger = logging.getLogger(__name__)


HW_IDS: list[str] = [
    "FIRST",
    "SECOND",
]

SECOND_HW_PARAM: list[str] = ["!ZZZ!", "!XXX!"]


OTHER_PARAMETERS: list[str] = ["p_1_1", "p_1_2"]
OTHER_PARAMETERS_2: list[str] = ["p_2_1", "p_2_2"]


calls_cache = collections.Counter()


@pytest.fixture(scope="session")
def heavy_fixture(request: pytest.FixtureRequest) -> str:
    param1, param2 = request.param
    calls_cache[(param1, param2)] += 1
    logger.info(f" ************ {param1} {param2} - {calls_cache[(param1, param2)]}")
    return f"{param1}/{param2}"

    
@pytest.fixture(scope="function", autouse=True)
def print_current_test_name(request: pytest.FixtureRequest):
    yield
    logger.info(f"-> {request.node.name}")

    
@pytest.fixture(scope="session", autouse=True)
def print_calls_cache():
    yield
    logger.info(" Fixture calls cache:")
    for (param1, param2), calls in calls_cache.items():
        logger.info(f" - {param1} {param2} -> {calls}")
    logger.info(f" Total times fixtures called: {sum(calls_cache.values())}")
    logger.info(f" Total heavy fixtures: {len(calls_cache)}")


@pytest.fixture(scope="module")
def fixture_1() -> str:
    return "fixture_1"


@pytest.fixture(scope="module")
def fixture_param_1(request: pytest.FixtureRequest) -> str:
    return request.param


@pytest.mark.parametrize(
    "heavy_fixture",
    [(m, b) for m in HW_IDS for b in SECOND_HW_PARAM],
    ids=lambda p: f"{p[0]}/{p[1]}",
    indirect=["heavy_fixture"],
)
def test_one_fxt(heavy_fixture: str, fixture_1):
    pass


@pytest.mark.parametrize("config", OTHER_PARAMETERS)
@pytest.mark.parametrize(
    "heavy_fixture",
    [(HW_IDS[0], SECOND_HW_PARAM[0])],
    ids=lambda p: f"{p[0]}/{p[1]}",
    indirect=["heavy_fixture"],
)
def test_with_params_one(heavy_fixture: str, config: str):    
    pass


@pytest.mark.parametrize("config", OTHER_PARAMETERS)
@pytest.mark.parametrize("config_2", OTHER_PARAMETERS_2)
@pytest.mark.parametrize(
    "heavy_fixture",
    [(HW_IDS[0], SECOND_HW_PARAM[1])],
    ids=lambda p: f"{p[0]}/{p[1]}",
    indirect=["heavy_fixture"],
)
def test_with_params_one_second_param(heavy_fixture: str, config: str, config_2: str):    
    pass


@pytest.mark.parametrize("config", OTHER_PARAMETERS)
@pytest.mark.parametrize(
    "heavy_fixture",
    [(HW_IDS[0], b) for b in SECOND_HW_PARAM],
    ids=lambda p: f"{p[0]}/{p[1]}",
    indirect=["heavy_fixture"],
)
def test_with_params_all_models(heavy_fixture: str, config: str):    
    pass


ITEMS_DATA = [
    ((hw, b), "fp_1") for hw in HW_IDS for b in SECOND_HW_PARAM
]


ITEMS_DATA_2 = [
    ((hw, b), "fp_2") for hw in HW_IDS for b in SECOND_HW_PARAM
]


def multiple_fixture_parametrize(
    items: tuple[tuple[str, str], str]
):
    return pytest.mark.parametrize(
        "heavy_fixture, fixture_param_1",
        items,
        indirect=["heavy_fixture", "fixture_param_1"],
        ids=[f"{item[0][0]}/{item[0][1]}/{item[1]}" for item in items]
    )
    
    

@multiple_fixture_parametrize(ITEMS_DATA)
def test_multiple_fixture_parametrize(heavy_fixture: str, fixture_param_1: str):
    pass

    

@multiple_fixture_parametrize(ITEMS_DATA_2)
def test_multiple_fixture_parametrize_second(heavy_fixture: str, fixture_param_1: str):
    pass

Actual Behavior

When running the tests, the output shows that heavy_fixture is called multiple times for the same parameters. For example, heavy_fixture with parameters ('FIRST', '!ZZZ!') is instantiated three separate times during the session.

Execution Log Snippet:

************ FIRST !ZZZ! - 1
> test_one_fxt[FIRST/!ZZZ!]
> test_with_params_one[FIRST/!ZZZ!-p_1_1]
> test_with_params_one[FIRST/!ZZZ!-p_1_2]
************ FIRST !XXX! - 1
> test_with_params_one_second_param[FIRST/!XXX!-p_2_1-p_1_1]
> test_with_params_one_second_param[FIRST/!XXX!-p_2_1-p_1_2]
> test_with_params_one_second_param[FIRST/!XXX!-p_2_2-p_1_1]
> test_with_params_one_second_param[FIRST/!XXX!-p_2_2-p_1_2]
************ FIRST !ZZZ! - 2
> test_with_params_all_models[FIRST/!ZZZ!-p_1_1]
> test_with_params_all_models[FIRST/!ZZZ!-p_1_2]
************ FIRST !XXX! - 2
> test_one_fxt[FIRST/!XXX!]
> test_with_params_all_models[FIRST/!XXX!-p_1_1]
> test_with_params_all_models[FIRST/!XXX!-p_1_2]
************ SECOND !ZZZ! - 1
> test_one_fxt[SECOND/!ZZZ!]
************ SECOND !XXX! - 1
> test_one_fxt[SECOND/!XXX!]
************ FIRST !ZZZ! - 3
> test_multiple_fixture_parametrize[FIRST/!ZZZ!/fp_1]
> test_multiple_fixture_parametrize_second[FIRST/!ZZZ!/fp_2]
************ FIRST !XXX! - 3
> test_multiple_fixture_parametrize[FIRST/!XXX!/fp_1]
> test_multiple_fixture_parametrize_second[FIRST/!XXX!/fp_2]
************ SECOND !ZZZ! - 2
> test_multiple_fixture_parametrize[SECOND/!ZZZ!/fp_1]
> test_multiple_fixture_parametrize_second[SECOND/!ZZZ!/fp_2]
************ SECOND !XXX! - 2
> test_multiple_fixture_parametrize[SECOND/!XXX!/fp_1]
> test_multiple_fixture_parametrize_second[SECOND/!XXX!/fp_2]

Final Fixture Call Count:

Fixture calls cache:
- FIRST !ZZZ! -> 3
- FIRST !XXX! -> 3
- SECOND !ZZZ! -> 2
- SECOND !XXX! -> 2
Total times fixtures called: 10
Total heavy fixtures: 4

Expected Behavior

Pytest should group all tests using the same session-scoped fixture instance together to avoid re-initialization. The heavy_fixture for a given parameter set (e.g., ('FIRST', '!ZZZ!') or ('FIRST', '!XXX!') ) should be set up only once for the entire test session.

Expected Execution Log:

 ************ FIRST !ZZZ! - 1
> test_one_fxt[FIRST/!ZZZ!]
...
> test_with_params_all_models[FIRST/!ZZZ!-p_1_1]
...
> test_multiple_fixture_parametrize[FIRST/!ZZZ!/fp_1]
...
 ************ FIRST !XXX! - 1
...

Expected Final Fixture Call Count:

 Fixture calls cache:
- FIRST !ZZZ! -> 1
- FIRST !XXX! -> 1
- SECOND !ZZZ! -> 1
- SECOND !XXX! -> 1
 ...

Additional Context

Interestingly, the execution order changes if the test function test_with_params_all_models is moved before other tests in the file (e.g., to line 97). When moved, the grouping for the ('FIRST', '!XXX!') parameter set behaves as expected (at least for this test), and its fixture is called one time less. This suggests that the test ordering algorithm is sensitive to the declaration order of tests, which can lead to inefficient fixture management.

How to run

[pytest]
log_cli = true
log_cli_level = DEBUG
pytest -v | grep 'INFO'

Environment

  • pytest 8.4.1
  • Ubuntu 24.04

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions