Functions That Raise Exceptions
A Python function that is handed bad input should say so by raising an exception: on success it returns its result, on failure it raises an error — such as ValueError — that explains what went wrong.
A test for such a function must check both paths: good input returns the right value and raises nothing, and bad input really does raise — a function that silently accepts bad input is a bug too.
pytest.raises is the tool for the bad path: a with block that fails the test unless the expected exception comes out of it.
The Good Path
For a valid input there is nothing extra to write. Call the function, then compare the value. If the call raises instead, the exception ends the test by itself and the report shows it — you do not have to check for it first.
Valid input, no exception
safe_divide(10, 2) must return 5 and must not raise. The call comes first; an unexpected exception would end the test right there. Only then is the value compared.
Output: complete Python program
def safe_divide(a, b):
if b == 0:
raise ValueError("cannot divide by zero")
return a / b
def test_safe_divide_good():
got = safe_divide(10, 2)
assert got == 5
The Bad Path
The other half of the contract: bad input must raise. The call goes inside a with pytest.raises(ValueError) block — if the function returns a number instead of raising, the test fails.
Naming the exception class matters: the block only accepts a ValueError, so a different, unexpected error still fails the test.
Bad input must raise
Dividing by zero has to fail. pytest.raises(ValueError) wraps the call safe_divide(10, 0). The function raises as promised, so the test passes.
Output: complete Python program
import pytest
def safe_divide(a, b):
if b == 0:
raise ValueError("cannot divide by zero")
return a / b
def test_safe_divide_by_zero():
with pytest.raises(ValueError):
safe_divide(10, 0)
A Table with want_error
Both paths fit into one parametrized test: give each case a want_error flag saying whether an exception is expected. The want value is None for that case, because a case that raises has no value to compare.
The test looks at want_error first. The expected failure is checked with pytest.raises and stops there; the other cases compare the value as usual.
Good and bad cases in one table
Four cases: three normal divisions and one division by zero with want_error=True. One parametrized test covers the whole contract of the function, and all four cases pass.
Output: complete Python program
import pytest
def safe_divide(a, b):
if b == 0:
raise ValueError("cannot divide by zero")
return a / b
@pytest.mark.parametrize("a, b, want, want_error", [
(10, 2, 5, False),
(-9, 3, -3, False),
(0, 5, 0, False),
(10, 0, None, True),
], ids=["simple_division", "negative_result", "divide_zero", "divide_by_zero"])
def test_safe_divide(a, b, want, want_error):
if want_error:
with pytest.raises(ValueError):
safe_divide(a, b)
else:
assert safe_divide(a, b) == want