Skip to content

Commit 1e02797

Browse files
authored
Improve pytest.raises docs (#11578)
1 parent 13e5ef0 commit 1e02797

File tree

3 files changed

+93
-21
lines changed

3 files changed

+93
-21
lines changed

doc/en/how-to/assert.rst

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,27 @@ and if you need to have access to the actual exception info you may use:
9898
the actual exception raised. The main attributes of interest are
9999
``.type``, ``.value`` and ``.traceback``.
100100

101+
Note that ``pytest.raises`` will match the exception type or any subclasses (like the standard ``except`` statement).
102+
If you want to check if a block of code is raising an exact exception type, you need to check that explicitly:
103+
104+
105+
.. code-block:: python
106+
107+
def test_recursion_depth():
108+
def foo():
109+
raise NotImplementedError
110+
111+
with pytest.raises(RuntimeError) as excinfo:
112+
foo()
113+
assert type(excinfo.value) is RuntimeError
114+
115+
The :func:`pytest.raises` call will succeed, even though the function raises :class:`NotImplementedError`, because
116+
:class:`NotImplementedError` is a subclass of :class:`RuntimeError`; however the following `assert` statement will
117+
catch the problem.
118+
119+
Matching exception messages
120+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
121+
101122
You can pass a ``match`` keyword parameter to the context-manager to test
102123
that a regular expression matches on the string representation of an exception
103124
(similar to the ``TestCase.assertRaisesRegex`` method from ``unittest``):
@@ -115,9 +136,15 @@ that a regular expression matches on the string representation of an exception
115136
with pytest.raises(ValueError, match=r".* 123 .*"):
116137
myfunc()
117138
118-
The regexp parameter of the ``match`` parameter is matched with the ``re.search``
119-
function, so in the above example ``match='123'`` would have worked as
120-
well.
139+
Notes:
140+
141+
* The ``match`` parameter is matched with the :func:`re.search`
142+
function, so in the above example ``match='123'`` would have worked as well.
143+
* The ``match`` parameter also matches against `PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``.
144+
145+
146+
Matching exception groups
147+
~~~~~~~~~~~~~~~~~~~~~~~~~
121148

122149
You can also use the :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>`
123150
method to test for exceptions returned as part of an ``ExceptionGroup``:
@@ -165,32 +192,55 @@ exception at a specific level; exceptions contained directly in the top
165192
assert not excinfo.group_contains(RuntimeError, depth=2)
166193
assert not excinfo.group_contains(TypeError, depth=1)
167194
168-
There's an alternate form of the :func:`pytest.raises` function where you pass
169-
a function that will be executed with the given ``*args`` and ``**kwargs`` and
170-
assert that the given exception is raised:
195+
Alternate form (legacy)
196+
~~~~~~~~~~~~~~~~~~~~~~~
197+
198+
There is an alternate form where you pass
199+
a function that will be executed, along ``*args`` and ``**kwargs``, and :func:`pytest.raises`
200+
will execute the function with the arguments and assert that the given exception is raised:
171201

172202
.. code-block:: python
173203
174-
pytest.raises(ExpectedException, func, *args, **kwargs)
204+
def func(x):
205+
if x <= 0:
206+
raise ValueError("x needs to be larger than zero")
207+
208+
209+
pytest.raises(ValueError, func, x=-1)
175210
176211
The reporter will provide you with helpful output in case of failures such as *no
177212
exception* or *wrong exception*.
178213

179-
Note that it is also possible to specify a "raises" argument to
180-
``pytest.mark.xfail``, which checks that the test is failing in a more
214+
This form was the original :func:`pytest.raises` API, developed before the ``with`` statement was
215+
added to the Python language. Nowadays, this form is rarely used, with the context-manager form (using ``with``)
216+
being considered more readable.
217+
Nonetheless, this form is fully supported and not deprecated in any way.
218+
219+
xfail mark and pytest.raises
220+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
221+
222+
It is also possible to specify a ``raises`` argument to
223+
:ref:`pytest.mark.xfail <pytest.mark.xfail ref>`, which checks that the test is failing in a more
181224
specific way than just having any exception raised:
182225

183226
.. code-block:: python
184227
228+
def f():
229+
raise IndexError()
230+
231+
185232
@pytest.mark.xfail(raises=IndexError)
186233
def test_f():
187234
f()
188235
189-
Using :func:`pytest.raises` is likely to be better for cases where you are
190-
testing exceptions your own code is deliberately raising, whereas using
191-
``@pytest.mark.xfail`` with a check function is probably better for something
192-
like documenting unfixed bugs (where the test describes what "should" happen)
193-
or bugs in dependencies.
236+
237+
This will only "xfail" if the test fails by raising ``IndexError`` or subclasses.
238+
239+
* Using :ref:`pytest.mark.xfail <pytest.mark.xfail ref>` with the ``raises`` parameter is probably better for something
240+
like documenting unfixed bugs (where the test describes what "should" happen) or bugs in dependencies.
241+
242+
* Using :func:`pytest.raises` is likely to be better for cases where you are
243+
testing exceptions your own code is deliberately raising, which is the majority of cases.
194244

195245

196246
.. _`assertwarns`:

doc/en/reference/reference.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,9 @@ Marks a test function as *expected to fail*.
249249
:keyword str reason:
250250
Reason why the test function is marked as xfail.
251251
:keyword Type[Exception] raises:
252-
Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test.
252+
Exception class (or tuple of classes) expected to be raised by the test function; other exceptions will fail the test.
253+
Note that subclasses of the classes passed will also result in a match (similar to how the ``except`` statement works).
254+
253255
:keyword bool run:
254256
Whether the test function should actually be executed. If ``False``, the function will always xfail and will
255257
not be executed (useful if a function is segfaulting).

src/_pytest/python_api.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -804,11 +804,13 @@ def raises( # noqa: F811
804804
def raises( # noqa: F811
805805
expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
806806
) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
807-
r"""Assert that a code block/function call raises an exception.
807+
r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
808808
809809
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
810810
The expected exception type, or a tuple if one of multiple possible
811-
exception types are expected.
811+
exception types are expected. Note that subclasses of the passed exceptions
812+
will also match.
813+
812814
:kwparam str | typing.Pattern[str] | None match:
813815
If specified, a string containing a regular expression,
814816
or a regular expression object, that is tested against the string
@@ -826,13 +828,13 @@ def raises( # noqa: F811
826828
.. currentmodule:: _pytest._code
827829
828830
Use ``pytest.raises`` as a context manager, which will capture the exception of the given
829-
type::
831+
type, or any of its subclasses::
830832
831833
>>> import pytest
832834
>>> with pytest.raises(ZeroDivisionError):
833835
... 1/0
834836
835-
If the code block does not raise the expected exception (``ZeroDivisionError`` in the example
837+
If the code block does not raise the expected exception (:class:`ZeroDivisionError` in the example
836838
above), or no exception at all, the check will fail instead.
837839
838840
You can also use the keyword argument ``match`` to assert that the
@@ -845,7 +847,7 @@ def raises( # noqa: F811
845847
... raise ValueError("value must be 42")
846848
847849
The ``match`` argument searches the formatted exception string, which includes any
848-
`PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``:
850+
`PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``:
849851
850852
>>> with pytest.raises(ValueError, match=r'had a note added'): # doctest: +SKIP
851853
... e = ValueError("value must be 42")
@@ -860,6 +862,20 @@ def raises( # noqa: F811
860862
>>> assert exc_info.type is ValueError
861863
>>> assert exc_info.value.args[0] == "value must be 42"
862864
865+
.. warning::
866+
867+
Given that ``pytest.raises`` matches subclasses, be wary of using it to match :class:`Exception` like this::
868+
869+
with pytest.raises(Exception): # Careful, this will catch ANY exception raised.
870+
some_function()
871+
872+
Because :class:`Exception` is the base class of almost all exceptions, it is easy for this to hide
873+
real bugs, where the user wrote this expecting a specific exception, but some other exception is being
874+
raised due to a bug introduced during a refactoring.
875+
876+
Avoid using ``pytest.raises`` to catch :class:`Exception` unless certain that you really want to catch
877+
**any** exception raised.
878+
863879
.. note::
864880
865881
When using ``pytest.raises`` as a context manager, it's worthwhile to
@@ -872,7 +888,7 @@ def raises( # noqa: F811
872888
>>> with pytest.raises(ValueError) as exc_info:
873889
... if value > 10:
874890
... raise ValueError("value must be <= 10")
875-
... assert exc_info.type is ValueError # this will not execute
891+
... assert exc_info.type is ValueError # This will not execute.
876892
877893
Instead, the following approach must be taken (note the difference in
878894
scope)::
@@ -891,6 +907,10 @@ def raises( # noqa: F811
891907
892908
See :ref:`parametrizing_conditional_raising` for an example.
893909
910+
.. seealso::
911+
912+
:ref:`assertraises` for more examples and detailed discussion.
913+
894914
**Legacy form**
895915
896916
It is possible to specify a callable by passing a to-be-called lambda::

0 commit comments

Comments
 (0)