Testing a function against five different inputs shouldn't require five nearly-identical copy-pasted test functions. pytest.mark.parametrize runs the same test logic once per input set, keeping the test DRY and making it trivial to add another case.
1Why Multiple Assert Statements in One Test Fall Short
Testing a function against several different inputs inside a single test function with multiple assert statements has two real, compounding problems. First, Python's assert statement halts execution the *instant* it fails ā a failing second assertion means the third and fourth assertions never even run during that test execution, so a single test run can hide additional failures that would only surface once the first one is fixed, extending debugging into multiple rounds instead of seeing every actual failure at once.
Second, and just as significant: when that second assertion fails, pytest's failure report identifies the specific line and the specific values involved (thanks to assertion rewriting from the pytest fundamentals lesson), but the test as a *whole* is reported as one single pass/fail unit named test_is_even ā there's no separate, named record distinguishing 'the is_even(3) case failed' from 'the whole test failed', which becomes genuinely confusing to parse once a test covers more than a couple of cases.
The alternative ā copy-pasting the test function once per input, each with a different assert ā solves the 'which case failed' problem (each copy fails or passes independently, clearly named), but reintroduces the DRY concern this platform has emphasized throughout: four near-identical function bodies, differing only in their hardcoded input and expected value, is exactly the kind of duplication that makes a later change (updating what the test actually checks) require editing four separate, easy-to-miss locations consistently.
# Unclear which case fails, and adding a case means editing this SAME function
def test_is_even():
assert is_even(2) == True
assert is_even(3) == False
assert is_even(0) == True
assert is_even(-4) == True
# If the SECOND assert fails, pytest just says 'AssertionError' -- not WHICH caseStops at first failure; unclear reporting; OR copy-pasted duplication
2parametrize: One Test Definition, Many Independent Cases
@pytest.mark.parametrize("number, expected", [(2, True), (3, False), ...]) solves both problems simultaneously. The decorator's first argument names the parameters the test function will receive (number, expected, matching the test function's own parameter names exactly); the second argument is a list of tuples, each providing one complete set of values for those parameters. pytest generates one genuinely independent test *invocation* per tuple ā not four assertions inside one test run, but four separate, independently-executed and independently-reported tests.
This directly solves the visibility problem: pytest's test report names each case distinctly (test_is_even[2-True], test_is_even[3-False], and so on, derived automatically from the parameter values), so a single failing case is immediately identifiable by name in the test output, without needing to trace back through which assert statement in a longer function happened to fail first. Because each case is a fully separate test execution, all four run to completion regardless of whether an earlier one fails ā you see every failing case from a single test run, not just the first one encountered.
And it directly solves the duplication problem: the actual test *logic* ā assert is_even(number) == expected ā exists exactly once, in one function body, no matter how many cases are covered. Adding a new case is a genuinely one-line change (appending one more tuple to the list), with zero risk of a copy-pasted duplicate silently drifting out of sync with the others over time.
import pytest
@pytest.mark.parametrize("number, expected", [
(2, True),
(3, False),
(0, True),
(-4, True),
])
def test_is_even(number, expected):
assert is_even(number) == expected
# pytest reports FOUR separate tests: test_is_even[2-True], test_is_even[3-False], etc.One test body, zero duplication, clear per-case reporting
3When Parametrization Genuinely Pays Off
Parametrization earns its value specifically when a function's core logic is being tested against meaningfully varied inputs that exercise the same code path ā boundary conditions (0, negative numbers, very large numbers for a numeric function), different branches of the same conditional logic, or a range of valid and invalid inputs for a validation function. In these cases, the underlying assertion logic (assert is_even(number) == expected) is genuinely identical across every case; only the specific input and expected output differ ā exactly the shape parametrize is designed for.
It's worth recognizing when parametrization *isn't* the right fit: if different inputs genuinely require meaningfully different assertion logic, setup, or verification steps (not just different input/output values plugged into the same check), forcing them into one parametrized test can produce a confusing, overly-generic test function trying to handle cases that don't actually share a common shape ā separate, clearly-named test functions remain the better choice for genuinely distinct scenarios.
Parametrize also composes cleanly with fixtures (the previous lesson) ā a parametrized test can still request fixtures normally alongside its parametrized values, and multiple @pytest.mark.parametrize decorators can even be stacked on the same test function to test the Cartesian product of two independent sets of parameters, directly echoing the itertools.product pattern from the Advanced Standard Library module, now applied specifically to generating test case combinations.
@pytest.mark.parametrize("number, expected", [
(2, True),
(3, False),
(0, True),
(-4, True),
(1_000_000, True), # just add another tuple -- that's the entire change
])
def test_is_even(number, expected):
assert is_even(number) == expectedThe right fit for parametrize ā genuinely different logic per case is not
4Step-by-Step Breakdown
Five copy-pasted test functions differing only in their input values is exactly the kind of duplication parametrize exists to eliminate ā one test definition, five (or five hundred) cases.
Without parametrize, testing multiple cases means either one test with several asserts (unclear WHICH case failed) or copy-pasted near-identical test functions.
Checkpoint: If the SECOND assert (is_even(3) == False) fails inside a single test function with four assert statements, what does pytest's failure report actually tell you?
- āIt reports the failure location precisely enough to identify the line, but doesn't distinguish this as a separate, independently-named failing 'case' the way parametrize would
- āIt clearly lists all four cases and marks specifically which ones passed and which failed
@pytest.mark.parametrize runs the SAME test body once per input tuple -- each case is reported as its OWN separate, independently pass/fail test.
Checkpoint: With @pytest.mark.parametrize and 4 input tuples, how many independently pass/fail-reported tests does pytest actually run?
- āFour separate tests, each independently reported as passing or failing, and each continuing even if another one fails
- āStill just one combined test, the same as writing four asserts manually
Adding a new test case is now a ONE-LINE change -- no new function, no copy-pasting, no risk of the new copy silently diverging from the others.
Parameterized tests cover input variety efficiently; Test Coverage measures how much of your actual code all these tests collectively exercise.
Run a Real Parameterized Test Suite. Finish run_parameterized_tests(): one test body, many inputs.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Use parametrize whenever testing the same logic against multiple, meaningfully different inputs
It eliminates copy-pasted near-duplicate test functions, and pytest reports each case independently with a clear, distinct name identifying exactly which input failed.
Don't force genuinely different test scenarios into one parametrized test just to avoid writing separate functions
Parametrize fits cases sharing identical assertion logic with only the input/output values varying ā scenarios needing genuinely different setup or verification steps are better served by separate, clearly-named test functions.
Frequent Bugs
Writing multiple separate assert statements testing different input cases inside a single test function, causing execution to stop at the first failure and hiding whether subsequent cases would have also failed.
Use @pytest.mark.parametrize to run each input case as an independent test, so every case executes and reports its own pass/fail status regardless of whether other cases fail.
Real-World Examples
Parameterized Tests for an Input Validation Function
A validate_email function needs to be tested against many valid and invalid email formats, and the team wants clear, individual reporting for exactly which specific format fails if a regression is introduced.
import pytest
@pytest.mark.parametrize("email, expected_valid", [
("user@example.com", True),
("invalid-email", False),
("user@", False),
("user+tag@example.co.uk", True),
("", False),
])
def test_validate_email(email, expected_valid):
assert validate_email(email) == expected_valid