Mathew K Analytics

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.…

⬇ Download notebook

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

pytest 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)
============================= test session starts =============================
collecting ... collected 3 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpecc2wq6p_test.py::test_is_even_two PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpecc2wq6p_test.py::test_is_even_three PASSED [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpecc2wq6p_test.py::test_is_even_four PASSED [100%]

============================== 3 passed in 0.19s ==============================
0

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)
============================= test session starts =============================
collecting ... collected 4 items

..\..\..\Users\makmw\AppData\Local\Temp\tmp7dnk3ieu_test.py::test_is_even_true_cases[2] PASSED [ 25%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp7dnk3ieu_test.py::test_is_even_true_cases[4] PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp7dnk3ieu_test.py::test_is_even_true_cases[6] PASSED [ 75%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp7dnk3ieu_test.py::test_is_even_true_cases[8] PASSED [100%]

============================== 4 passed in 0.12s ==============================
0

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)
============================= test session starts =============================
collecting ... collected 4 items

..\..\..\Users\makmw\AppData\Local\Temp\tmp58wh1_ke_test.py::test_add_cases[2-3-5] PASSED [ 25%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp58wh1_ke_test.py::test_add_cases[0-0-0] PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp58wh1_ke_test.py::test_add_cases[-1-1-0] PASSED [ 75%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp58wh1_ke_test.py::test_add_cases[10--5-5] PASSED [100%]

============================== 4 passed in 0.11s ==============================
0

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)
============================= test session starts =============================
collecting ... collected 3 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpeo5sauhj_test.py::test_classify[negative-number] PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpeo5sauhj_test.py::test_classify[exactly-zero] PASSED [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpeo5sauhj_test.py::test_classify[positive-number] PASSED [100%]

============================== 3 passed in 0.10s ==============================
0

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)
============================= test session starts =============================
collecting ... collected 6 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpd1vj5bnu_test.py::test_multiply_stacked[10-1] PASSED [ 16%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpd1vj5bnu_test.py::test_multiply_stacked[10-2] PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpd1vj5bnu_test.py::test_multiply_stacked[10-3] PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpd1vj5bnu_test.py::test_multiply_stacked[100-1] PASSED [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpd1vj5bnu_test.py::test_multiply_stacked[100-2] PASSED [ 83%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpd1vj5bnu_test.py::test_multiply_stacked[100-3] PASSED [100%]

============================== 6 passed in 0.12s ==============================
0

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)
============================= test session starts =============================
collecting ... collected 3 items

..\..\..\Users\makmw\AppData\Local\Temp\tmp9in5aomk_test.py::test_safe_divide[10-2-5] PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp9in5aomk_test.py::test_safe_divide[9-3-3] PASSED [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmp9in5aomk_test.py::test_safe_divide[5-0-None] XFAIL [100%]

======================== 2 passed, 1 xfailed in 0.31s =========================
0

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)
============================= test session starts =============================
collecting ... collected 6 items

..\..\..\Users\makmw\AppData\Local\Temp\tmpvxlzkj6l_test.py::test_validate_username[valid-alpha] PASSED [ 16%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpvxlzkj6l_test.py::test_validate_username[valid-alnum] PASSED [ 33%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpvxlzkj6l_test.py::test_validate_username[too-short] PASSED [ 50%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpvxlzkj6l_test.py::test_validate_username[too-long] PASSED [ 66%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpvxlzkj6l_test.py::test_validate_username[has-space] PASSED [ 83%]
..\..\..\Users\makmw\AppData\Local\Temp\tmpvxlzkj6l_test.py::test_validate_username[empty] PASSED [100%]

============================== 6 passed in 0.15s ==============================
0

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.