Skip to content

Match causing exceptions with pytest.raises #12763

Closed

Description

@MarcBresson

read more about causing / context exceptions in the python doc

What's the problem this feature will solve?

It is quite unintuitive to match a causing exceptions (raise Exception("i'm an exception") from Exception("I'm the causing exception")) with pytest.

As for an example, imagine you want to match a ValueError with the message "i'm a causing exception"

def i_raise_a_causing_exception():
    raise RuntimeError("i'm an exception") from ValueError("i'm a causing exception")

If you want to do that now, you have to use the workaround described below.

Describe the solution you'd like

pytest.raises could have an option to match the causing/context exception. Of course, now comes the problem of recursivity since causing/context exceptions can themselves be causing exceptions.

This would allow us to do something along the lines of

def test_catch_cause_exception():
    with pytest.raises(ValueError, match="i'm a causing exception", cause=True)

if we have multiple caused exception chained together, we could also have:

def i_raise_a_causing_exception():
    try:
        raise ValueError("i'm the 1st causing exception") from ValueError("i'm the 2nd causing exception")
    except ValueError as e:
        raise ValueError("i'm an exception") from e

def test_catch_cause_exception():
    with pytest.raises(ValueError, match="i'm the 3rd causing exception", cause=2):
        i_raise_a_causing_exception()

Of course, we would have the same thing for context exceptions:

def i_raise_a_context_exception():
    try:
        try:
            raise ValueError("This is the first context")
        finally:
            raise ValueError("This is the second context")
    finally:
        raise ValueError("This is the cause")

def test_catch_context_exception():
    with pytest.raises(ValueError, match="This is the second context", context=2):
        i_raise_a_context_exception()

Alternative Solutions

I just found a workaround in https://stackoverflow.com/a/78939835/12550791 (disclaimer, i'm the author of the question and of the solution).

def test_catch_cause_exception():
    # match any exception
    with pytest.raises(Exception) as exc:
        i_raise_a_causing_exception()

    # match the cause exception
    with pytest.raises(ValueError, match="i'm a causing exception"):
        # in case the first exception caught does not have a cause.
        # This will mark the test a failed with `did not raise`.
        if exc.value.__cause__ is not None:
            raise exc.value.__cause__

However, if you have multiple level of cause exceptions you will have to rely on recursivity or make one with the mess.

Additional context

Activity

  1. The-Compiler commented on Sep 2, 2024

    @The-Compiler
    Member

    The match= argument is a shorthand for the common case. I'm -1 on trying to shoehorn all kinds of special cases in such a shorthand, since what you describe - even more so when considering multiple levels - seems quite exotic.

    You can always just access the exception object itself and act on that instead:

    import pytest
    
    
    def i_raise_a_causing_exception():
        raise RuntimeError("i'm an exception") from ValueError("i'm a causing exception")
    
    
    def test_exception():
        with pytest.raises(RuntimeError) as excinfo:
            i_raise_a_causing_exception()
    
        assert str(excinfo.value.__cause__) == "i'm a causing exception"
  2. MarcBresson commented on Sep 2, 2024

    @MarcBresson
    ContributorAuthor

    the issue with assert str(excinfo.value.__cause__) == "i'm a causing exception" is that we do not benefit from the special error did not raise. Besides, it makes the user use the - very unknown - __cause__ or __context__ attribute of exceptions.

  3. RonnyPfannschmidt commented on Sep 2, 2024

    @RonnyPfannschmidt
    Member

    having a matcher object that expresses this explicit with a explcit api thats not barking up the wrong tree of DWIM is the way to go here, matcher objects are still pre-planning and in early discussion

  4. MarcBresson commented on Sep 2, 2024

    @MarcBresson
    ContributorAuthor

    oh! Would love to see matcher objects!

    Should I close this issue then?

  5. RonnyPfannschmidt commented on Sep 2, 2024

    @RonnyPfannschmidt
    Member

    We should link it up with the other discussions first

  6. added
    type: proposalproposal for a new feature, often to gather opinions or design the API around the new feature
    on Oct 30, 2024
  7. jakkdl commented on Mar 5, 2025

    @jakkdl
    Member

    #13192 allows this with

    import pytest
    
    def foo():
        raise ValueError("foo") from TypeError("bar")
    
    def test_foo():
        with pytest.raises(
            ValueError,
            match="^foo$",
            check=lambda e: pytest.raises(TypeError, match="^bar$").matches(e.__cause__),
        ):
            foo()
  8. linked a pull request that will close this issueadd RaisesGroup & Matcher #13192on Mar 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: proposalproposal for a new feature, often to gather opinions or design the API around the new feature

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions