pytest.raises and parametrize
pytest.raises is a check that passes only when the code inside it raises a given error, and parametrize runs one test with many inputs.
Last updated: 30 Sep, 2026 · Python 3.14 · pytest 9.1 · Pydantic 2.12
The tests in pytest check answers that are right. The code most likely to fail on the job handles answers that are wrong, so it needs tests too.
Syntax:
with pytest.raises(SomeError):
code_that_should_fail()
@pytest.mark.parametrize("value", [a, b, c])
def test_x(value):
...The checking step in its own file
The check from model_validate_json, as a function in parse.py:
from typing import Literal
from pydantic import BaseModel, Field
class Triage(BaseModel):
category: Literal["billing", "shipping", "other"]
priority: int = Field(ge=1, le=5)
def parse_answer(answer: str) -> Triage:
return Triage.model_validate_json(answer)Expecting an error with pytest.raises
import pytest
from pydantic import ValidationError
from parse import parse_answer
def test_good_answer():
result = parse_answer('{"category": "billing", "priority": 4}')
assert result.category == "billing"
def test_sentence_is_rejected():
with pytest.raises(ValidationError):
parse_answer("I am not sure how to sort this one.")with pytest.raises(ValidationError): passes only if the code inside raises that error. If parse_answer ever starts accepting sentences, this test fails and says so.
pytest -q.. [100%] 2 passed in 0.05s
Running one test with many inputs
There are many ways for JSON to be wrong: a category that does not exist, a priority out of range, a field missing. Writing a test function for each repeats the same three lines. Add this to the end of test_parse.py:
@pytest.mark.parametrize("answer", [
'{"category": "sales", "priority": 4}',
'{"category": "billing", "priority": 9}',
'{"category": "billing"}',
])
def test_bad_values_are_rejected(answer):
with pytest.raises(ValidationError):
parse_answer(answer)@pytest.mark.parametrize is a decorator, like @dataclass in dataclasses. It runs the test once for each value in the list, passing it in as answer.
pytest -q..... [100%] 5 passed in 0.04s
Five tests from two functions and one list. Adding a new kind of bad answer, when you meet one in real use, is one line in that list.
Listing the tests with --collect-only
pytest --collect-only -qtest_parse.py::test_good_answer
test_parse.py::test_sentence_is_rejected
test_parse.py::test_bad_values_are_rejected[{"category": "sales", "priority": 4}]
test_parse.py::test_bad_values_are_rejected[{"category": "billing", "priority": 9}]
test_parse.py::test_bad_values_are_rejected[{"category": "billing"}]
5 tests collected in 0.04s--collect-only lists the tests without running them. Each parametrized run is named after the function plus the input in square brackets, so a failure tells you exactly which answer broke it.
try and except vs pytest.raises
try / except | pytest.raises | |
|---|---|---|
| Used in | Your program | Your tests |
| No error raised | The program carries on | The test fails |
| Purpose | Recover from the error | Prove the error happens |
Where these show up in AI code
- A list of bad model answers you have met, each one a parametrized case.
- Proving that a validator, a guardrail or a parser refuses what it should.
with pytest.raises block, never runs. Keep one call per block, or a second check will look tested when it is not.Related
- Previous: pytest
- Next: Project: ticket triage
- Reference: Parametrizing tests in the pytest docs
- Add
'{"category": "billing", "priority": "high"}'to the list. - Add
'{"category": "billing", "priority": "4"}'. Why does that case fail the test? - Change
ge=1toge=0inparse.pyand runpytest -q. No test notices; add a case with priority 0 to the list so one does.
You understood something today that you didn't yesterday.