pytest parametrize: expect an error in some cases (tested)
Pass the expectation as a parameter: pytest.raises(ValueError, match=...) for inputs that must fail and contextlib.nullcontext() (imported as does_not_raise) for inputs that must not, then call the code under with expectation:. A wrong case fails with DID NOT RAISE or the real exception.
To test valid and invalid inputs in one parametrized test, put the expectation itself in the parameters: pytest.raises(...) for cases that must raise and contextlib.nullcontext() for cases that must not, then run the code inside with expectation:. Measured with pytest 9.1.1 on Python 3.13.5.
1. The pattern: pytest.raises or nullcontext per case
$ cat test_pe.py
from contextlib import nullcontext as does_not_raise
import pytest
def parse_age(s):
n = int(s)
if n < 0:
raise ValueError(f"age must be >= 0, got {n}")
return n
@pytest.mark.parametrize("value, expectation, expected", [
("42", does_not_raise(), 42),
("-3", pytest.raises(ValueError, match="must be >= 0"), None),
("abc", pytest.raises(ValueError, match="invalid literal"), None),
])
def test_parse_age(value, expectation, expected):
with expectation:
assert parse_age(value) == expected
$ pytest -v -p no:cacheprovider test_pe.py | grep -E "PASSED|FAILED|passed|failed"
test_pe.py::test_parse_age[42-expectation0-42] PASSED [ 33%]
test_pe.py::test_parse_age[-3-expectation1-None] PASSED [ 66%]
test_pe.py::test_parse_age[abc-expectation2-None] PASSED [100%]
============================== 3 passed in 0.02s ===============================
All three cases pass: the valid input returns 42 and the two invalid inputs raise ValueError with the expected message (match= is a regex search). Importing nullcontext as does_not_raise is the name used in the pytest docs and reads well in the parameter list.
2. What a wrong expectation looks like
$ cat test_wrong.py
from contextlib import nullcontext as does_not_raise
import pytest
from test_pe import parse_age
@pytest.mark.parametrize("value, expectation", [
("7", pytest.raises(ValueError)), # wrong: "7" is valid
("-1", does_not_raise()), # wrong: "-1" raises
], ids=["seven", "minus-one"])
def test_wrong(value, expectation):
with expectation:
parse_age(value)
$ pytest -q -p no:cacheprovider --tb=line test_wrong.py | tail -6
E ValueError: age must be >= 0, got -1
/tmp/pe388/test_pe.py:7: ValueError: age must be >= 0, got -1
=========================== short test summary info ============================
FAILED test_wrong.py::test_wrong[seven] - Failed: DID NOT RAISE ValueError
FAILED test_wrong.py::test_wrong[minus-one] - ValueError: age must be >= 0, g...
2 failed in 0.01s
A case that should raise but does not fails with Failed: DID NOT RAISE ValueError; a case marked does_not_raise() that does raise fails with the real exception. ids= gives the cases readable names in the output instead of expectation0.
3. Alternative: pytest.param with xfail(raises=...)
$ cat test_xfail.py
import pytest
from test_pe import parse_age
@pytest.mark.parametrize("value, expected", [
("42", 42),
pytest.param("-3", None, marks=pytest.mark.xfail(raises=ValueError, strict=True)),
pytest.param("abc", None, marks=pytest.mark.xfail(raises=TypeError, strict=True)),
])
def test_marks(value, expected):
assert parse_age(value) == expected
$ pytest -v -p no:cacheprovider --tb=line test_xfail.py | grep -E "PASSED|FAILED|XFAIL|passed|failed|^/|Error"
test_xfail.py::test_marks[42-42] PASSED [ 33%]
test_xfail.py::test_marks[-3-None] XFAIL [ 66%]
test_xfail.py::test_marks[abc-None] FAILED [100%]
E ValueError: invalid literal for int() with base 10: 'abc'
/tmp/pe388/test_pe.py:5: ValueError: invalid literal for int() with base 10: 'abc'
FAILED test_xfail.py::test_marks[abc-None] - ValueError: invalid literal for ...
==================== 1 failed, 1 passed, 1 xfailed in 0.02s ====================
xfail(raises=ValueError, strict=True) reports the case as XFAIL when it raises that type, and as FAILED when it raises something else (the third case expected TypeError but got ValueError). This works, but XFAIL means "known bug" in reports, and it cannot check the message, so for intended errors prefer the pytest.raises pattern from section 1.
Related: pytest.raises: get and check the error message, pytest Failed: DID NOT RAISE, pytest parametrize: function uses no argument.
Frequently asked questions
How do I expect an exception in only some pytest parametrize cases?
Add a parameter holding pytest.raises(SomeError) for failing cases and contextlib.nullcontext() for passing ones, and run the code inside with expectation:.
What is does_not_raise in pytest?
Not a pytest function: it is contextlib.nullcontext imported under that name, a context manager that does nothing, so any exception inside it fails the test.
Can I use xfail for expected errors in parametrize?
pytest.param(..., marks=pytest.mark.xfail(raises=ValueError, strict=True)) works, but it reports XFAIL and cannot check the message; pytest.raises is the clearer choice for intended errors.
Written with AI assistance. Measured on 2026-10-23 with pytest 9.1.1 on Python 3.13.5 in a clean virtualenv. Each block was run verbatim and the output pasted unedited. PlainHeaders is our extension; pytest is free.