pytest.mark.parametrize: test many cases with one function
Design a small table of input-output cases and run it as separate named pytest cases in a local project.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: pytest.mark.parametrize: test many cases with one function
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
pytest.mark.parametrize turns one test function into a separate test invocation for each row of case data. This removes copy-pasted tests while keeping each input visible in the results. The decorator names the parameters, then supplies tuples with values for those names. For example, @pytest.mark.parametrize('text, expected', [('0', 0), ('12', 12)]) runs the test body twice. Today we will test the claim with a tiny example, inspect a misleading first attempt, and finish with a verification case. Design a small table of input-output cases and run it as separate named pytest cases in a local project.
- 00:42
A useful case table covers boundaries and representative inputs, not only the happy path. The local example tests zero, a multi- digit value, surrounding whitespace, negative input, and invalid text. It separates successful values from inputs expected to raise ValueError so that each test has one clear assertion. The ids argument gives human-readable names in pytest output, making a failure easier to locate. The first run is intentionally incomplete or incorrect. Each new input requires another duplicated function. Read the actual output before editing the code; a familiar-looking command can still
- 01:22
answer the wrong question. Parametrization begins with a clear table of inputs and expected outputs. A tuple ('12', 12) means passing the string twelve to the parser should return the integer twelve. Include zero because it is a boundary, a multi-digit value because it is common, and surrounding spaces because int accepts them. Keep the table small enough that each row has a reason to exist. The browser example runs this table with a plain Python comprehension. That confirms the production function and each expected value without depending on pytest inside the page.
- 02:02
When you move to a local test file, use the same rows in @pytest.mark.parametrize('text, expected', cases). The decorator produces one test invocation per row rather than one loop with a single report. We use a second run to make the cause visible. The printed values are small enough to check by hand. If they disagree with your prediction, pause and identify the assumption that changed. A test suite earns its value when it finds a mismatch. The deliberately incorrect expectation in this example says text '2' should become 3. The parser returns 2, so the assertion fails. A plain loop would stop at that row.
- 02:44
With pytest parametrize, the row is reported as a separate failed case while other rows can still run. Use ids such as zero, two-digits, and spaces to make results understandable without reading raw tuple representations. IDs describe the scenario; they do not alter test behavior. Keep production code outside the test body, and avoid adding branching logic to the test just to make one row pass. If a row reveals a real contract mismatch, correct the function or expectation. Now compare the corrected run with the earlier one. The important change is the rule encoded in the operation, not merely the final display.
- 03:26
Run the code and inspect the returned values as well as their interpretation. pytest is a third-party local test runner. Save the complete example as test_parse_numbers.py, install it with python -m pip install pytest, then run python -m pytest -q test_parse_numbers.py. The website's browser Python console does not install or launch pytest. Its practice task instead builds and checks the same case table with ordinary Python, so it works without a local test runner. That exercise is the data preparation for the decorator, not a claim that the browser executed pytest.
- 04:06
A parser also has an error contract. Negative numbers are rejected by our function, while letters and empty text fail at int conversion. All three raise ValueError, but for different reasons. A local pytest file can parametrize these invalid inputs and put the call inside with pytest.raises(ValueError). That creates a separate check for every invalid scenario. The browser demonstration uses try/except only to make the three outcomes visible without a test runner. Do not write a bare except in production tests, because it can hide unrelated programming errors. The complete local example at the top of this page uses pytest.raises and names
- 04:48
each invalid row. Install pytest in a local environment, run python -m pytest -q, and inspect individual case IDs when one fails. The final run checks a different condition so a special-case answer cannot pass unnoticed. Try the linked browser task and use the guide when you need a smaller step. The source links on the lesson page give the complete API behavior. Avoid a single test that loops over all rows when diagnosing a large table locally: the first failing assertion stops that loop, while parametrize reports each row as an individual case.
- 05:28
Keep tests deterministic and small. If cases need setup, use fixtures rather than hiding side effects inside the data table. A test case should state the contract and leave production code separate from test definitions. When the local test runner shows a failure, read the case ID together with the assertion diff. An ID such as spaces tells you the row involved without opening the entire test table. Keep the expected value literal and independent from the implementation: computing the expected result by calling the same function would make the test circular. Parameter tables can cover many inputs, but more rows are not automatically
- 06:08
better. Include boundaries, invalid classes, and previously observed bugs. For every row, ask what behavior it protects and remove redundant cases that add run time without a new assertion.
Understand the concept
pytest.mark.parametrize turns one test function into a separate test invocation for each row of case data. This removes copy-pasted tests while keeping each input visible in the results. The decorator names the parameters, then supplies tuples with values for those names. For example, @pytest.mark.parametrize('text, expected', [('0', 0), ('12', 12)]) runs the test body twice.
A useful case table covers boundaries and representative inputs, not only the happy path. The local example tests zero, a multi-digit value, surrounding whitespace, negative input, and invalid text. It separates successful values from inputs expected to raise ValueError so that each test has one clear assertion. The ids argument gives human-readable names in pytest output, making a failure easier to locate.
pytest is a third-party local test runner. Save the complete example as test_parse_numbers.py, install it with python -m pip install pytest, then run python -m pytest -q test_parse_numbers.py. The website's browser Python console does not install or launch pytest. Its practice task instead builds and checks the same case table with ordinary Python, so it works without a local test runner. That exercise is the data preparation for the decorator, not a claim that the browser executed pytest.
Avoid a single test that loops over all rows when diagnosing a large table locally: the first failing assertion stops that loop, while parametrize reports each row as an individual case. Keep tests deterministic and small. If cases need setup, use fixtures rather than hiding side effects inside the data table. A test case should state the contract and leave production code separate from test definitions.
- parametrize creates one test invocation per row
- decorator parameter names match tuple positions
- ids make failures readable
- pytest.raises checks expected exceptions
See it step by step
Read the code, predict the output, then compare it with the result.
01. Build a plain case table
def parse_nonnegative(text):
number = int(text)
if number < 0:
raise ValueError('negative number')
return number
cases = [('0', 0), ('12', 12), (' 7 ', 7)]
print([(text, parse_nonnegative(text) == expected) for text, expected in cases])[('0', True), ('12', True), (' 7 ', True)]Each tuple contains one input and its expected result; the local pytest decorator consumes the same shape.
02. Expose a wrong expected value
def parse_nonnegative(text):
return int(text)
try:
assert parse_nonnegative('2') == 3
except AssertionError:
print('case failed: text=2 expected=3')case failed: text=2 expected=3A named pytest case would report this row separately rather than hide it inside a longer loop.
03. Check invalid input behavior in plain Python
def parse_nonnegative(text):
number = int(text)
if number < 0:
raise ValueError('negative number')
return number
for text in ['-1', 'oops', '']:
try:
parse_nonnegative(text)
except ValueError:
print(repr(text), 'rejected')'-1' rejected
'oops' rejected
'' rejectedThe local pytest example expresses each of these as its own pytest.raises case.
Run real pytest cases locally
Save this code as test_parse_numbers.py. Install pytest with python -m pip install pytest, then run python -m pytest -q test_parse_numbers.py. The browser console practices the case table using plain Python; it does not run pytest.
import pytest
def parse_nonnegative(text):
number = int(text)
if number < 0:
raise ValueError('negative number')
return number
@pytest.mark.parametrize(
'text, expected',
[('0', 0), ('12', 12), (' 7 ', 7)],
ids=['zero', 'two-digits', 'spaces'],
)
def test_valid(text, expected):
assert parse_nonnegative(text) == expected
@pytest.mark.parametrize(
'text', ['-1', 'oops', ''],
ids=['negative', 'letters', 'empty'],
)
def test_invalid(text):
with pytest.raises(ValueError):
parse_nonnegative(text)A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Write the cases before the decorator
Parametrization begins with a clear table of inputs and expected outputs. A tuple ('12', 12) means passing the string twelve to the parser should return the integer twelve. Include zero because it is a boundary, a multi-digit value because it is common, and surrounding spaces because int accepts them. Keep the table small enough that each row has a reason to exist.
The browser example runs this table with a plain Python comprehension. That confirms the production function and each expected value without depending on pytest inside the page. When you move to a local test file, use the same rows in @pytest.mark.parametrize('text, expected', cases). The decorator produces one test invocation per row rather than one loop with a single report.
def parse_nonnegative(text):
number = int(text)
if number < 0:
raise ValueError('negative number')
return number
cases = [('0', 0), ('12', 12), (' 7 ', 7)]
print([(text, parse_nonnegative(text) == expected) for text, expected in cases])[('0', True), ('12', True), (' 7 ', True)]- Run the three-case table.
- Identify which tuple component is the input and which is expected output.
Make a failing row easy to identify
A test suite earns its value when it finds a mismatch. The deliberately incorrect expectation in this example says text '2' should become 3. The parser returns 2, so the assertion fails. A plain loop would stop at that row. With pytest parametrize, the row is reported as a separate failed case while other rows can still run.
Use ids such as zero, two-digits, and spaces to make results understandable without reading raw tuple representations. IDs describe the scenario; they do not alter test behavior. Keep production code outside the test body, and avoid adding branching logic to the test just to make one row pass. If a row reveals a real contract mismatch, correct the function or expectation.
def parse_nonnegative(text):
return int(text)
try:
assert parse_nonnegative('2') == 3
except AssertionError:
print('case failed: text=2 expected=3')case failed: text=2 expected=3- Run the intentionally wrong assertion.
- Describe the scenario name you would give this row.
Test invalid inputs as their own cases
A parser also has an error contract. Negative numbers are rejected by our function, while letters and empty text fail at int conversion. All three raise ValueError, but for different reasons. A local pytest file can parametrize these invalid inputs and put the call inside with pytest.raises(ValueError). That creates a separate check for every invalid scenario.
The browser demonstration uses try/except only to make the three outcomes visible without a test runner. Do not write a bare except in production tests, because it can hide unrelated programming errors. The complete local example at the top of this page uses pytest.raises and names each invalid row. Install pytest in a local environment, run python -m pytest -q, and inspect individual case IDs when one fails.
def parse_nonnegative(text):
number = int(text)
if number < 0:
raise ValueError('negative number')
return number
for text in ['-1', 'oops', '']:
try:
parse_nonnegative(text)
except ValueError:
print(repr(text), 'rejected')'-1' rejected
'oops' rejected
'' rejected- Run the invalid values.
- Explain why an unexpected exception type should not be silently accepted.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Loop over text, expected in cases, assert parse_nonnegative(text) == expected, and return len(cases).
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Copying nearly identical tests
test_zero(), test_twelve(), test_spaces() with repeated bodies@pytest.mark.parametrize('text, expected', rows)What happens: Each new input requires another duplicated function.
Keep one assertion body and enumerate varied inputs as rows.
Only checking successful values
rows = [('1', 1), ('2', 2)]Add separate parametrized invalid cases using pytest.raises(ValueError).What happens: Negative and malformed input behavior stays untested.
The error contract is part of the function's behavior.