It is sometimes necessary to test whether your code raises the right exception under certain conditions. Pytest provides several powerful ways to test and verify exceptions in your code.
This article will walk you through all the techniques for expecting and asserting exceptions in Pytest, complete with practical examples and best practices.
Basic Exception Testing with pytest.raises
The primary tool for exception testing is the pytest.raises context manager. its basic syntax is as follows:
import pytest with pytest.raises(ExpectedException): #code to be tested
consider the following example:
#tests.py import pytest def test_division_by_0(): with pytest.raises(ZeroDivisionError): 1/0
If you run the pytest command, you will see that the above test passes asserting that the ZeroDivisionError was indeed raised.
(venv) C:\Users\John\Desktop>pytest tests.py
================================================= test session starts =================================================
platform win32 -- Python 3.11.6, pytest-8.3.4, pluggy-1.5.0
rootdir: C:\Users\John\Desktop
plugins: anyio-4.8.0, django-4.10.0
collected 1 item
tests.py . [100%]
================================================== 1 passed in 0.07s ==================================================
If the expected exception is not raised the test will fail.
#tests.py import pytest def test_division_by_0(): with pytest.raises(ZeroDivisionError): 1/0
collected 1 item
tests.py F [100%]
====================================================== FAILURES =======================================================
_________________________________________________ test_division_by_0 __________________________________________________
def test_division_by_0():
> with pytest.raises(ZeroDivisionError):
E Failed: DID NOT RAISE <class 'ZeroDivisionError'>
tests.py:4: Failed
=============================================== short test summary info ===============================================
FAILED tests.py::test_division_by_0 - Failed: DID NOT RAISE <class 'ZeroDivisionError'>
================================================== 1 failed in 0.93s ==================================================
This above test fails because the code block doesn't raise a ZeroDivisionError, as expected.
Verifying Exception Messages
You can also check the exception message using the match parameter. The test only passes if the expected exception is raised with the specified message.
The match string can contain a regular expression
#tests.py def test_exception_message(): with pytest.raises(ValueError, match="invalid literal for.*XYZ'$"): int("XYZ")
collected 1 item
tests.py . [100%]
================================================== 1 passed in 0.07s ==================================================
The above test verifies both that a ValueError is raised and that the error message matches the pattern.
Accessing the Exception Object
Sometimes you need to inspect the exception object itself. You can capture it using the as clause:
with pytest.raises(ExpectedException) as <alias_name>: #tests
#tests.py def test_exception_attributes(): with pytest.raises(KeyError) as exc_info: D = {} D['missing_key'] assert exc_info.value.args[0] == 'missing_key'
collected 1 item
tests.py . [100%]
================================================== 1 passed in 0.07s ==================================================
The captured exception object contains details about the raised exception, including:
-
type: The exception class -
value: The exception instance -
traceback: The traceback object
Checking Multiple Exceptions
For functions that might raise different exceptions in different scenarios, you can pass a tuple containing multiple exception classes to pytest.raises. If any of the specified exceptions is raised, the test passes.
#tests.py import pytest import random def test_multiple_exceptions(): with pytest.raises((ValueError, TypeError)): if random.choice([True, False]): int("None") else: int(None)
collected 1 item
tests.py . [100%]
================================================== 1 passed in 0.07s ==================================================
Testing with Exception Hierarchies
Pytest matches exception hierarchies, this means that you can test whether an exception is raised using its parent exceptions. For example, both KeyError and IndexEerror are subclasses of the LookupError exception,(which is itself a subclass of the basic Exception class). You can therefore test whether a KeyError or an IndexError was raised by just using the LookupError exception.
#tests.py import pytest def test_exception_hierarchy(): with pytest.raises(LookupError): L = ['a', 'b', 'c'] L[10] #IndexError
collected 1 item
tests.py . [100%]
================================================== 1 passed in 0.20s ==================================================
As you might know most of the common exceptions are subclasses of the Exception class, you can use this exception to test that any non-exit exception was raised
Parametrized exception testing
You can combine pytest.mark.parametrize with exception testing for comprehensive coverage. For example:
import pytest @pytest.mark.parametrize("input,expected_exception", [ (None, TypeError), ("abc", ValueError), ("", ValueError), ]) def test_int_conversion(input, expected_exception): with pytest.raises(expected_exception): int(input)
collected 3 items
main.py ... [100%]
================================================== 3 passed in 0.19s ==================================================
Best Practices for Exception Testing
-
Be Specific: Catch the most specific exception possible rather than generic
Exception -
Keep Tests Focused: Each test should verify one exception scenario
-
Document Intent: Use descriptive test names that explain why the exception is expected
With these techniques in your testing toolkit, you'll be able to write more reliable Python code that handles error conditions properly and predictably.