Lesson 10 · Pytest deep dive
pytest Tutorial #10: Parametrised & Indirect Fixtures
On testing real Python code with pytest. Fixtures with params, request.param, and indirect parametrization. Let's get into it. @pytest.fixture(params=[...])…
- CoursePytest deep dive
- Lesson10 of 18
- FormatJupyter notebook · 7 code cells
What you'll learn
- The Helper From Last Time
- A Fixture With params - request.param
- Every Test Sharing the Fixture Multiplies Together
- Custom IDs on a Parametrized Fixture
- Indirect Parametrization - Values Flow Through a Fixture
- Indirect on Just One of Several Parameters
- A Real Pattern - Reusable Environment Configs
- Wrap-Up: What You Learned
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbpytest Deep-Dive, Video 10: Parametrized and Indirect Fixtures#
- Video ten of an eighteen-part series on testing real Python code with pytest.
- Fixtures with params, request.param, and indirect parametrization.
- Let's get into it.
Part 1: The Helper From Last Time#
import pytest, tempfile, os
def run_pytest(code_str, args=None):
with tempfile.NamedTemporaryFile(mode='w', suffix='_test.py', delete=False) as f:
f.write(code_str)
path = f.name
result = pytest.main([path, *(args or [])])
os.unlink(path)
return result
Part 2: A Fixture With params - request.param#
code = '''
import pytest
@pytest.fixture(params=[1, 2, 3])
def number(request):
return request.param
def test_number_is_positive(number):
assert number > 0
def test_number_squared(number):
assert number * number >= number
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 3: Every Test Sharing the Fixture Multiplies Together#
code = '''
import pytest
@pytest.fixture(params=['sqlite', 'postgres', 'mysql'])
def db_backend(request):
return request.param
def test_backend_is_known(db_backend):
assert db_backend in ('sqlite', 'postgres', 'mysql')
def test_backend_name_is_lowercase(db_backend):
assert db_backend == db_backend.lower()
def test_backend_name_not_empty(db_backend):
assert len(db_backend) > 0
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 4: Custom IDs on a Parametrized Fixture#
code = '''
import pytest
@pytest.fixture(params=[0, -1, 100], ids=['zero', 'negative', 'large'])
def edge_case_value(request):
return request.param
def test_edge_case_is_int(edge_case_value):
assert isinstance(edge_case_value, int)
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 5: Indirect Parametrization - Values Flow Through a Fixture#
code = '''
import pytest
@pytest.fixture
def doubled(request):
return request.param * 2
@pytest.mark.parametrize('doubled', [1, 2, 3], indirect=True)
def test_doubled_value(doubled):
assert doubled in (2, 4, 6)
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 6: Indirect on Just One of Several Parameters#
code = '''
import pytest
@pytest.fixture
def user_record(request):
name = request.param
return {'name': name, 'greeting': f'hello, {name}'}
@pytest.mark.parametrize('user_record, expected_length', [
('alice', 5),
('bob', 3),
], indirect=['user_record'])
def test_user_record_name_length(user_record, expected_length):
assert len(user_record['name']) == expected_length
assert user_record['greeting'].startswith('hello,')
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 7: A Real Pattern - Reusable Environment Configs#
code = '''
import pytest
ENVIRONMENTS = [
{'name': 'dev', 'debug': True, 'workers': 1},
{'name': 'staging', 'debug': True, 'workers': 2},
{'name': 'prod', 'debug': False, 'workers': 4},
]
@pytest.fixture(params=ENVIRONMENTS, ids=lambda e: e['name'])
def env_config(request):
return request.param
def test_env_has_positive_workers(env_config):
assert env_config['workers'] > 0
def test_prod_has_debug_off(env_config):
if env_config['name'] == 'prod':
assert env_config['debug'] is False
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Wrap-Up: What You Learned#
- @pytest.fixture(params=[...]) makes the fixture itself run once per value, via request.param.
- Every test requesting a parametrized fixture automatically multiplies by that fixture value count.
- ids works on parametrized fixtures too, giving each value a readable label in verbose output.
- indirect=True on parametrize routes values through a matching fixture instead of straight to the test.
- indirect also accepts a list of names, routing only those specific parametrize arguments through fixtures.
- A parametrized fixture of environment configs avoids repeating the same parametrize list on every test.
- That wraps up parametrized and indirect fixtures. Next up: Plugins.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



