Lesson 3 · Pytest deep dive
pytest Tutorial #3: Parametrize — Many Cases, One Test
On testing real Python code with pytest. @pytest.mark.parametrize: real one test function, many real input cases. Let's get into it.…
- CoursePytest deep dive
- Lesson3 of 18
- FormatJupyter notebook · 8 code cells
What you'll learn
Data
No separate download needed — the notebook creates or downloads everything it uses.
📓 Full notebook
Download .ipynbpytest Deep-Dive, Video 3: Parametrize#
- Video three of an eighteen-part series on testing real Python code with pytest.
- @pytest.mark.parametrize: real one test function, many real input cases.
- 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: Why Parametrize - the Repeated-Test Problem#
code = '''
def is_even(n):
return n % 2 == 0
def test_is_even_two():
assert is_even(2) is True
def test_is_even_three():
assert is_even(3) is False
def test_is_even_four():
assert is_even(4) is True
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 3: A Basic @pytest.mark.parametrize#
code = '''
import pytest
def is_even(n):
return n % 2 == 0
@pytest.mark.parametrize('n', [2, 4, 6, 8])
def test_is_even_true_cases(n):
assert is_even(n) is True
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 4: Multiple Parameters Per Case#
code = '''
import pytest
def add(a, b):
return a + b
@pytest.mark.parametrize('a, b, expected', [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
(10, -5, 5),
])
def test_add_cases(a, b, expected):
assert add(a, b) == expected
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 5: Custom Test IDs#
code = '''
import pytest
def classify(n):
if n < 0:
return 'negative'
if n == 0:
return 'zero'
return 'positive'
@pytest.mark.parametrize('n, expected', [
(-5, 'negative'),
(0, 'zero'),
(5, 'positive'),
], ids=['negative-number', 'exactly-zero', 'positive-number'])
def test_classify(n, expected):
assert classify(n) == expected
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 6: Stacking Parametrize - a Cartesian Product#
code = '''
import pytest
def multiply(a, b):
return a * b
@pytest.mark.parametrize('a', [1, 2, 3])
@pytest.mark.parametrize('b', [10, 100])
def test_multiply_stacked(a, b):
result = multiply(a, b)
assert result == a * b
assert result > 0
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 7: pytest.param() - Per-Case Marks#
code = '''
import pytest
def safe_divide(a, b):
return a / b
@pytest.mark.parametrize('a, b, expected', [
(10, 2, 5),
(9, 3, 3),
pytest.param(5, 0, None, marks=pytest.mark.xfail(reason="division by zero")),
])
def test_safe_divide(a, b, expected):
assert safe_divide(a, b) == expected
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Part 8: A Real Pattern - Valid and Invalid Input Cases#
code = '''
import pytest
def validate_username(name):
if not isinstance(name, str):
return False
if not (3 <= len(name) <= 20):
return False
return name.isalnum()
@pytest.mark.parametrize('name, expected', [
('alice', True),
('bob123', True),
('ab', False),
('a' * 21, False),
('has space', False),
('', False),
], ids=['valid-alpha', 'valid-alnum', 'too-short', 'too-long', 'has-space', 'empty'])
def test_validate_username(name, expected):
assert validate_username(name) == expected
'''
exit_code = run_pytest(code, ['-v', '--no-header'])
print(exit_code)
Wrap-Up: What You Learned#
- @pytest.mark.parametrize turns one test function into many cases, avoiding near-identical duplicated tests.
- A comma-separated argument-name string paired with a list of tuples supplies multiple values per case.
- ids gives each case a readable, meaningful name instead of the default auto-generated one.
- Stacking two parametrize decorators on the same function produces the cartesian product of both value sets.
- pytest.param() wraps a single case so a mark, like xfail, or a custom id can apply to just that one case.
- One parametrized test with valid and invalid cases thoroughly covers a function from a single definition.
- That wraps up parametrize. Next up: Mocking and monkeypatch.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



