Skip to content

pytest.exit() not working when in ExceptionGroup #13650

Description

@mtrzm

Hi

I have a situation where I have two teardown fixtures:

  • one raises exception
  • second performs pytest.exit() - I use it to stop testing session when only specific test fails.

Simplified example:

import pytest

@pytest.fixture
def failing_teardown():
    yield
    raise IOError("Exception in teardown")

@pytest.fixture
def exit_session():
    yield 
    pytest.exit("Forced exit")

def test_1(): return

@pytest.mark.usefixtures("failing_teardown", "exit_session")
def test_failure(): return

def test_3(): return

My expectation is that:

  • test_1 passes
  • test_2 call passes, teardown reports error (due to exception in failing_teardown) and test session finishes due to pytest.exit() in exit_session fixture
  • test_3 is not executed

But the exception from _pytest.outcomes.Exit is only logged inside ExceptionGroup and exit is not performed:

$ pytest
================================= test session starts ==================================
platform linux -- Python 3.13.3, pytest-8.4.1, pluggy-1.6.0
rootdir: ./pytest-exit-fail
plugins: ordering-0.6
collected 3 items                                                                                                                                                                                                            

test_dummy.py ..E.                                                                                                                                                                                                     [100%]

======================================== ERRORS ========================================
__________________________ ERROR at teardown of test_failure ___________________________
  + Exception Group Traceback (most recent call last):
  |   File ".venv/lib/python3.13/site-packages/_pytest/runner.py", line 344, in from_call
  |     result: TResult | None = func()
  |                              ~~~~^^
  |   File ".venv/lib/python3.13/site-packages/_pytest/runner.py", line 246, in <lambda>
  |     lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
  |             ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
  |   File ".venv/lib/python3.13/site-packages/pluggy/_hooks.py", line 512, in __call__
  |     return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
  |            ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File ".venv/lib/python3.13/site-packages/pluggy/_manager.py", line 120, in _hookexec
  |     return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  |            ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File ".venv/lib/python3.13/site-packages/pluggy/_callers.py", line 167, in _multicall
  |     raise exception
  |   File ".venv/lib/python3.13/site-packages/pluggy/_callers.py", line 139, in _multicall
  |     teardown.throw(exception)
  |     ~~~~~~~~~~~~~~^^^^^^^^^^^
  |   File ".venv/lib/python3.13/site-packages/_pytest/logging.py", line 858, in pytest_runtest_teardown
  |     yield
  |   File ".venv/lib/python3.13/site-packages/pluggy/_callers.py", line 139, in _multicall
  |     teardown.throw(exception)
  |     ~~~~~~~~~~~~~~^^^^^^^^^^^
  |   File ".venv/lib/python3.13/site-packages/_pytest/capture.py", line 905, in pytest_runtest_teardown
  |     return (yield)
  |             ^^^^^
  |   File ".venv/lib/python3.13/site-packages/pluggy/_callers.py", line 121, in _multicall
  |     res = hook_impl.function(*args)
  |   File ".venv/lib/python3.13/site-packages/_pytest/runner.py", line 193, in pytest_runtest_teardown
  |     item.session._setupstate.teardown_exact(nextitem)
  |     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
  |   File ".venv/lib/python3.13/site-packages/_pytest/runner.py", line 557, in teardown_exact
  |     raise exceptions[0]
  | ExceptionGroup: errors while tearing down <Function test_failure> (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File ".venv/lib/python3.13/site-packages/_pytest/runner.py", line 546, in teardown_exact
    |     fin()
    |     ~~~^^
    |   File ".venv/lib/python3.13/site-packages/_pytest/fixtures.py", line 1068, in finish
    |     raise exceptions[0]
    |   File ".venv/lib/python3.13/site-packages/_pytest/fixtures.py", line 1057, in finish
    |     fin()
    |     ~~~^^
    |   File ".venv/lib/python3.13/site-packages/_pytest/fixtures.py", line 938, in _teardown_yield_fixture
    |     next(it)
    |     ~~~~^^^^
    |   File "./test_dummy.py", line 7, in failing_teardown
    |     raise IOError("Exception in teardown")
    | OSError: Exception in teardown
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File ".venv/lib/python3.13/site-packages/_pytest/runner.py", line 546, in teardown_exact
    |     fin()
    |     ~~~^^
    |   File ".venv/lib/python3.13/site-packages/_pytest/fixtures.py", line 1068, in finish
    |     raise exceptions[0]
    |   File ".venv/lib/python3.13/site-packages/_pytest/fixtures.py", line 1057, in finish
    |     fin()
    |     ~~~^^
    |   File ".venv/lib/python3.13/site-packages/_pytest/fixtures.py", line 938, in _teardown_yield_fixture
    |     next(it)
    |     ~~~~^^^^
    |   File "./test_dummy.py", line 13, in exit_session
    |     pytest.exit("Forced exit")
    |     ~~~~~~~~~~~^^^^^^^^^^^^^^^
    |   File ".venv/lib/python3.13/site-packages/_pytest/outcomes.py", line 122, in exit
    |     raise Exit(reason, returncode)
    | _pytest.outcomes.Exit: Forced exit
    +------------------------------------
=============================== short test summary info ================================
ERROR test_dummy.py::test_failure - ExceptionGroup: errors while tearing down <Function test_failure> (2 sub-exceptions)
============================== 3 passed, 1 error in 0.01s ==============================

If I remove failing_teardown fixture, pytest.exit() works:

import pytest


@pytest.fixture
def exit_session():
    yield 
    pytest.exit("Forced exit")


def test_1(): return

@pytest.mark.usefixtures("exit_session")
def test_failure(): return

def test_3(): return

log:

$ pytest
================================= test session starts ==================================
platform linux -- Python 3.13.3, pytest-8.4.1, pluggy-1.6.0
rootdir: ./pytest-exit-fail
plugins: ordering-0.6
collected 3 items                                                                                                                                                                                                            

test_dummy.py ..

!!!!!!!!!!!!!!!!!!!!!!!!!! _pytest.outcomes.Exit: Forced exit !!!!!!!!!!!!!!!!!!!!!!!!!!
================================== 2 passed in 0.15s ===================================

Environment:

  • platform: linux
  • Python 3.13.3
  • pytest-8.4.1
  • pluggy-1.6.0

Activity

  1. added
    type: bugproblem that needs to be addressed
    needs backportapplied to PRs, indicates that it should be ported to the current bug-fix branch
    on Aug 14, 2025
  2. RonnyPfannschmidt commented on Aug 14, 2025

    @RonnyPfannschmidt
    Member

    nice find - this was most likely a oversight when teardown got migrated from plain exceptions to exceptiongroup

  3. The-Compiler commented on Aug 14, 2025

    @The-Compiler
    Member

    Also see #13537 which is mostly the same issue, but for skipping.

  4. sunalawa commented on Aug 26, 2025

    @sunalawa

    @RonnyPfannschmidt Do you need help to backport this?

  5. The-Compiler commented on Aug 26, 2025

    @The-Compiler
    Member

    Huh? There's nothing to backport as long as there's no fix, and most backports are automated.

  6. sunalawa commented on Aug 28, 2025

    @sunalawa

    Ohh.. Newbie here. I was not aware about this. I thought the "needs backport" meant help was needed to backport this.

  7. gomri15 commented on Sep 13, 2025

    @gomri15
    Contributor

    @RonnyPfannschmidt is this something i can take a shot at fixing ?. or due to the complexity of the issue and my lack of knowledge of the code base it's better left to someone more experienced?

  8. RonnyPfannschmidt commented on Sep 13, 2025

    @RonnyPfannschmidt
    Member

    I haven't assessed complexity

    The problem naively reads as handle exceotion groups in some places

    So its definitely a neat way to learn the codebase abd figure if the naive reading is good enough

  9. gomri15 commented on Sep 13, 2025

    @gomri15
    Contributor

    i would love to give this my best shot if, could you assign me to it please.

    and if you can point me to anywhere in the code base i should start i would really appreciate it. thank you.

  10. RonnyPfannschmidt commented on Sep 14, 2025

    @RonnyPfannschmidt
    Member

    The starting point would be to turn the failing examples into acceptance tests in a fitting place tgen following the traceback of those errors

  11. gomri15 commented on Sep 14, 2025

    @gomri15
    Contributor

    Thanks so much for the guidance, excited to start working on this

  12. gomri15 commented on Sep 18, 2025

    @gomri15
    Contributor

    Submitted a small fix, note it doesn't handle nested exception groups I thought it would over complicate the code for A unlikely edge case.

    if you think otherwise i can add support and tests for it,

  13. nevin-lim commented on Oct 3, 2025

    @nevin-lim

    Hi there, my team and i would like to work on this issue if that is alright with you?

  14. gomri15 commented on Oct 3, 2025

    @gomri15
    Contributor

    Hi there, my team and i would like to work on this issue if that is alright with you?

    There is already a PR waiting for rereview on this :)

  15. gomri15 commented on Oct 3, 2025

    @gomri15
    Contributor

    Any chance of getting a review for this?.
    Apologizes if it's forward of me to ask, this is my first time attempting to contribute to open-source code and I am not sure what the etiquette or expected timeline is for a review.

    Please take no offence in my forwardness and see it as excitement to contribute and help make this great repo a little bit better.

  16. bluetech commented on Oct 15, 2025

    @bluetech
    Member

    I can see 3 possible semantics for this (an exception group which contains an exception that should be re-raised):

    1. If the EG contains a re-raise E, then re-raise the first one (this is what @gomri15 currently implemented)
    2. If the EG contains a re-raise E, then re-raise the entire EG
    3. If the EG contains more than one re-raise E, then re-raise them all in an EG

    Since the re-raise exceptions in pytest itself are stuff like Exit, KeyboardInterrupt, SystemExit, I feel pretty good about choice 1. Usually they'll just kill the session so there's no need to preserve anything beyond the first one.

    I searched for reraise and CallInfo.from_call in my "plugin corpus" and didn't find any different uses.

    So unless anyone thinks different I think we can merge PR #13736.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

needs backportapplied to PRs, indicates that it should be ported to the current bug-fix branchtype: bugproblem that needs to be addressed

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions