Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

Testing Expected Exceptions with `pytest.raises`

Recommended resources

Welcome to the next lesson in our journey to master professional Python testing. In our last session, we focused on pytest.mark.parametrize, a powerful tool for efficiently testing your code against numerous inputs. We briefly saw how it could be used to check for error conditions, which serves as a perfect entry point for today's topic.

This lesson is dedicated to robustly testing for failure. In professional software development, ensuring that your code fails predictably and informatively is just as critical as ensuring it succeeds. You will learn to use pytest.raises to assert that your code raises the correct exceptions under specific conditions, and, just as importantly, that the error messages produced are precise and helpful. This skill is fundamental for building resilient data pipelines and services where clear error reporting is non-negotiable.

The Basics: Testing for Expected Exceptions

A function should signal invalid input or an inability to perform its task by raising an exception. Your tests must verify this behavior. While you could write a try...except block to catch an exception and pass the test, this is verbose and clunky. pytest provides a much more elegant and expressive solution: the pytest.raises context manager.

The basic pattern is to wrap the code that should raise an error within a with pytest.raises(ExpectedExceptionType): block. If the specified exception is raised, the test passes. If a different exception is raised, or none at all, the test fails.

The video below, from the anthonywritescode channel, provides a great contrast between the old manual way of testing exceptions and the clean, modern pytest.raises approach.

pytest: testing exceptions (beginner - intermediate) anthony explains #176

This video introduces pytest.raises and demonstrates why it's superior to manual try/except blocks for testing exceptions.

Watch from the demonstration. Pay attention to how the verbose try/except/raise AssertionError pattern is replaced by a single, readable with statement.

Here is a simple example to illustrate the pattern:

import pytest

def get_inverse(x):
    if not isinstance(x, (int, float)):
        raise TypeError("Input must be a number")
    if x == 0:
        raise ValueError("Cannot compute the inverse of zero")
    return 1 / x

def test_get_inverse_raises_value_error_for_zero():
    with pytest.raises(ValueError):
        get_inverse(0)

def test_get_inverse_raises_type_error_for_string():
    with pytest.raises(TypeError):
        get_inverse("not a number")

If get_inverse(0) raises a ValueError as expected, test_get_inverse_raises_value_error_for_zero passes.


Verifying Specific Error Messages

Simply checking that the correct type of exception was raised is a good start, but it's often not enough. A function might raise a ValueError for several different reasons. To ensure your code is failing for the right reason, you need to inspect the error message itself. pytest.raises offers two primary ways to do this.

1. Using the match Parameter

The match parameter provides a concise way to assert that the exception's message matches a given string or regular expression. This is extremely useful for ensuring your error messages are consistent and informative.

This short clip from the "Tech With Tim" channel shows a clear example of using match to check for a specific error string.

Please Learn How To Write Tests in Python… • Pytest Tutorial

This video demonstrates how to use pytest.raises with the match argument to verify the content of an exception message.

Watch the segment from testing for errors. Note how the test fails when the match string doesn't align with the actual error message, proving its effectiveness.

The following article shows how mature projects like Celery use this feature, including with regular expressions for more flexible matching.

How to Use Pytest for Exception Testing

This article demonstrates the use of the match parameter with real-world examples from the Celery project.

Please read the section Using match. The second example from Celery, which uses a regular expression (match=r'.*The specified bucket does not exist'), is a powerful pattern for when parts of an error message might be dynamic.

Applying this to our example:

def test_get_inverse_raises_specific_value_error():
    with pytest.raises(ValueError, match="Cannot compute the inverse of zero"):
        get_inverse(0)

2. Inspecting the ExceptionInfo Object

For more complex validation, you can capture the raised exception into a variable. The pytest.raises context manager returns an ExceptionInfo object, which acts as a wrapper around the actual exception. By convention, this is often named exc_info or exc.

You can then access the exception instance itself via exc_info.value and perform detailed assertions on its attributes after the with block has completed. This is crucial for testing custom exceptions that might carry structured error data.

This article provides a clear introduction to this pattern.

How to Check if an Exception Is Raised (or Not) With pytest

This article walks through the process of capturing and inspecting an exception to validate its message.

Read the section Assert the Exception Message. Focus on the pattern with pytest.raises(...) as exc: and how exc.value is used to access the exception and its message.

The anthonywritescode video you watched earlier also contains an excellent, practical walkthrough of implementing this from scratch.

pytest: testing exceptions (beginner - intermediate) anthony explains #176

This segment shows how to capture the exception information and test a user-facing error message, explaining the best practices along the way.

Please watch from capturing the exception. The pattern exc_info.value.args[0] to get the message from a simple exception is common, and the advice to avoid logic in your test assertions is a key principle of professional testing.

This technique is especially powerful when dealing with libraries that provide rich error objects. For example, data validation libraries like Pydantic raise a ValidationError that contains a list of all validation failures. The following example from a real-world project demonstrates how to test this.

How to Use Pytest for Exception Testing

This part of the article shows a more advanced use case: inspecting the attributes of a captured exception from the Pydantic library.

Read the section on Inspecting Raised Exception Attributes. Note how the test makes assertions on the exc_info.value.errors() method outside the with block to verify the exact nature of the validation failures. This is a common and robust pattern in data engineering.


Combining Exception Testing with Parameterization

The true power of these techniques becomes apparent when you combine them with the parameterization we learned in the last lesson. This allows you to create a single, elegant test that verifies a wide range of invalid inputs and their corresponding exceptions.

The following example, drawn from the requests library, shows how to test for multiple types of invalid URL errors in one concise function.

How to Use Pytest for Exception Testing

This section demonstrates how to combine @pytest.mark.parametrize with pytest.raises to create a comprehensive, data-driven error test.

Read the section Using @pytest.mark.parametrize. This pattern is the gold standard for testing functions that can fail in multiple, predictable ways based on their input.


A Critical Best Practice: Be Specific

A common pitfall is to test for overly broad exceptions like Exception or BaseException. While the test might pass, it could be masking a completely different bug, such as a TypeError or AttributeError from a mistake in your test code itself. Always be as specific as possible with the exception type you expect.

The anthonywritescode video explains this anti-pattern perfectly.

pytest: testing exceptions (beginner - intermediate) anthony explains #176

This final clip explains why catching broad exceptions is a bad practice that can lead to silently passing but incorrect tests.

Watch the short segment on this "worst practice" from avoiding broad exceptions. This is a crucial piece of advice for writing reliable tests.


Conclusion

In this lesson, you've mastered the techniques for testing failure conditions in Python. Writing tests for how your code fails is a hallmark of a professional software engineer, leading to more robust, predictable, and maintainable systems.

Here are the key takeaways:

  • with pytest.raises(ExceptionType): is the standard, idiomatic way to assert that a block of code raises a specific exception.
  • match="pattern" is a concise and powerful parameter for verifying that the exception message conforms to an expected string or regular expression.
  • with pytest.raises(...) as exc_info: allows you to capture the exception details and perform complex assertions on its attributes (via exc_info.value) outside the with block.
  • Combining pytest.raises with @pytest.mark.parametrize enables you to create comprehensive, data-driven tests for all your error-handling logic.
  • Always be specific with your expected exception types to avoid masking other bugs.

Now that you can test your functions in isolation, both for success and failure, what happens when your function depends on external systems like a database, a file system, or a web API? In our next lesson, we will tackle this by learning how to mock external services and dependencies using the pytest-mock plugin, allowing you to unit test your code without relying on outside systems.