Typorithm · medium

Fixture scope and conftest ownership

pythonpytesttestingindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Fixture scope and conftest ownership

0%

Concept and contract

Reuse stable setup while giving each test its own mutable working copy.

Fixture scope controls how long a fixture instance is reused: function, class, module, package or session. A broader scope can avoid repeated expensive setup, but also shares its result among more tests. Shared mutable state needs deliberate ownership rather than a hope that tests never modify it.

A conftest.py makes fixtures available to tests in its directory tree without importing the fixture manually. For this runnable single-file example the fixture is local. In the playground, move its definitions to conftest.py and retain tests in test_main.py to see the discovery boundary directly.

Mechanism and worked example

The module-scoped fixture returns an immutable tuple of supported skills.

The module-scoped fixture returns an immutable tuple of supported skills. The function-scoped fixture makes a new list from that tuple for each test. Tests can append to their owned list without changing the shared baseline. This separates reusable configuration from mutable work.

A broader-scoped fixture cannot simply depend on a narrower-scoped fixture whose lifetime cannot satisfy it. A ScopeMismatch is a setup error, not a product assertion failure. Design the dependency graph from stable outer resources toward per-test state.

Complete worked example

python

import pytest


@pytest.fixture(scope="module")
def supported():
    return ("python", "sql")


@pytest.fixture
def skills(supported):
    return list(supported)


def test_initial_skills(skills):
    assert skills == ["python", "sql"]


def test_add_owned_skill(skills):
    skills.append("pytest")
    assert skills == ["python", "sql", "pytest"]
Reasoning
The module-scoped fixture returns an immutable tuple of supported skills. The function-scoped fixture makes a new list from that tuple for each test. Tests can append to their owned list without changing the shared baseline. This separates reusable configuration from mutable work. A broader-scoped fixture cannot simply depend on a narrower-scoped fixture whose lifetime cannot satisfy it. A ScopeMismatch is a setup error, not a product assertion failure. Design the dependency graph from stable outer resources toward per-test state.
Open in local Python playground ↗

Focused test decision: context is in the complete example

python

@pytest.fixture
def skills(supported):
    return list(supported)
Focused test decision
Type this short test decision after understanding the complete runnable module. Desktop typing records practice; Run tests in the local playground executes real pytest.

Counterexamples and common mistakes

The controlled baseline omits one required skill.

The controlled baseline omits one required skill. Both initial-state and append assertions reject it. Returning a shared mutable list and passing it straight through the function fixture would create a subtler order dependency even though the decorator says function scope.

Function scope means the fixture function runs per test; it does not guarantee that the function returns a newly owned object. A cached global, shared tuple containing mutable lists, or shallow copy of nested data can still leak mutations.

Controlled bug: the tests must report failure

python

import pytest


@pytest.fixture(scope="module")
def supported():
    return ("sql",)


@pytest.fixture
def skills(supported):
    return list(supported)


def test_initial_skills(skills):
    assert skills == ["python", "sql"]


def test_add_owned_skill(skills):
    skills.append("pytest")
    assert skills == ["python", "sql", "pytest"]
Reasoning
The controlled baseline omits one required skill. Both initial-state and append assertions reject it. Returning a shared mutable list and passing it straight through the function fixture would create a subtler order dependency even though the decorator says function scope. Function scope means the fixture function runs per test; it does not guarantee that the function returns a newly owned object. A cached global, shared tuple containing mutable lists, or shallow copy of nested data can still leak mutations.
Open in local Python playground ↗

Independent practice

Add a test that removes Python from its own working list and checks only SQL remains.

Add a test that removes Python from its own working list and checks only SQL remains. Retain the other tests. Then run the suite in different orders locally to confirm the ownership model rather than relying on file order.

For a small follow-up, move fixtures into conftest.py and verify the same tests are discovered. Do not import conftest as an application module. It belongs to pytest’s fixture-discovery mechanism, not the production dependency graph.

Step 1: Add an owned-list removal test without changing the shared baseline. Step 2: Keep the existing passing cases. Add the smallest new assertion first; predict the failure before running pytest.

Practice starter: solve before reading the solution

python

import pytest


@pytest.fixture(scope="module")
def supported():
    return ("python", "sql")


@pytest.fixture
def skills(supported):
    return list(supported)


def test_initial_skills(skills):
    assert skills == ["python", "sql"]


def test_add_owned_skill(skills):
    skills.append("pytest")
    assert skills == ["python", "sql", "pytest"]

# Task: Add an owned-list removal test without changing the shared baseline.
Reasoning
Add an owned-list removal test without changing the shared baseline.
Open in local Python playground ↗
  • Hint 1: Request skills, not the module-scoped supported fixture.
  • Hint 2: Remove python and assert the remaining list equals ["sql"].

Solution, tests and trade-offs

The solution adds an owned removal operation.

The solution adds an owned removal operation. The probe returns a list subclass whose remove method does nothing, preserving the existing initial-state and append behavior. The new assertion detects the missing effect.

This deliberately focused mutation tests the added regression case. It does not exhaust nested-copy problems. If the shared configuration becomes a structure containing mutable values, decide whether to build fresh values or use a deliberate deep copy and add tests for that ownership contract.

Practice solution and acceptance checks

python

import pytest


@pytest.fixture(scope="module")
def supported():
    return ("python", "sql")


@pytest.fixture
def skills(supported):
    return list(supported)


def test_initial_skills(skills):
    assert skills == ["python", "sql"]


def test_add_owned_skill(skills):
    skills.append("pytest")
    assert skills == ["python", "sql", "pytest"]

def test_remove_owned_skill(skills):
    skills.remove("python")
    assert skills == ["sql"]
Reasoning
The solution adds an owned removal operation. The probe returns a list subclass whose remove method does nothing, preserving the existing initial-state and append behavior. The new assertion detects the missing effect. This deliberately focused mutation tests the added regression case. It does not exhaust nested-copy problems. If the shared configuration becomes a structure containing mutable values, decide whether to build fresh values or use a deliberate deep copy and add tests for that ownership contract.
Open in local Python playground ↗

Interview reasoning and next step

Explain why immutable shared configuration and fresh per-test state can coexist.

Explain why immutable shared configuration and fresh per-test state can coexist. State that scope is a lifetime policy, while copying is an ownership decision. Describe where conftest fixtures are visible and why explicit, narrow dependencies are easier to debug than hidden global setup.

Next use skip and xfail with honest expectations. Scope-related setup errors should not be marked as expected product failures merely to make the suite appear healthy.

Optional stretch — after the core task works: Introduce one deliberate implementation mistake and explain which assertion catches it. A green run alone is not evidence that every possible bug is covered.

Computer lab: https://applydjinn.com/typorithm/pytest-lab-20261106.zip. Run verify.py in an isolated environment. Worked examples and solutions pass, controlled counterexamples fail, and the new practice assertions reject a mutation missed by the baseline. Run --practice pyq-scope after changing the supplied exercise. Python executes on your computer or in the browser, without submitting code to a server. Native question progress is a separate learning record.

Reuse stable setup while giving each test its own mutable working copy.