{"id":"62827611aec0a756","repo":"pytest-dev/pytest","slug":"multiple-thread-exception-warnings","errorCode":null,"errorMessage":"multiple thread exception warnings","messagePattern":"multiple thread exception warnings","errorType":"exception","errorClass":"ExceptionGroup","httpStatus":null,"severity":"warning","filePath":"src/_pytest/threadexception.py","lineNumber":72,"sourceCode":"                continue\n\n            msg = meta.msg\n            try:\n                warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))\n            except pytest.PytestUnhandledThreadExceptionWarning as e:\n                # This except happens when the warning is treated as an error (e.g. `-Werror`).\n                if meta.exc_value is not None:\n                    # Exceptions have a better way to show the traceback, but\n                    # warnings do not, so hide the traceback from the msg and\n                    # set the cause so the traceback shows up in the right place.\n                    e.args = (meta.cause_msg,)\n                    e.__cause__ = meta.exc_value\n                errors.append(e)\n\n        if len(errors) == 1:\n            raise errors[0]\n        if errors:\n            raise ExceptionGroup(\"multiple thread exception warnings\", errors)\n    finally:\n        del errors, meta, hook_error\n\n\ndef cleanup(\n    *, config: Config, prev_hook: Callable[[threading.ExceptHookArgs], object]\n) -> None:\n    try:\n        try:\n            # We don't join threads here, so exceptions raised from any\n            # threads still running by the time _threading_atexits joins them\n            # do not get captured (see #13027).\n            collect_thread_exception(config)\n        finally:\n            threading.excepthook = prev_hook\n    finally:\n        del config.stash[thread_exceptions]\n","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/pytest-dev/pytest/blob/98b357f69e380da908740a212288d73b2ee06687/src/_pytest/threadexception.py#L54-L90","documentation":"Raised as an ExceptionGroup during session teardown when more than one unhandled exception was captured from background threads during the test session. pytest installs a threading.excepthook that records exceptions raised by threads the test spawned but never joined; at session end collect_thread_exception re-emits them as PytestUnhandledThreadExceptionWarning, and when several accumulate they are bundled into this group.","triggerScenarios":"Tests spawn threads (threading.Thread, ThreadPoolExecutor workers, daemon threads, asyncio in a thread) that raise exceptions the main test never asserts on; multiple such tests run in one session and each leaks an exception.","commonSituations":"Migrating sync code to thread pools without handling worker exceptions; background scheduler/heartbeat threads in app code under test; flaky network calls in worker threads; CI runs with -Werror turning the warnings into hard failures.","solutions":["Join threads and propagate their exceptions: capture via a queue/future and re-raise in the main thread (e.g. futures.result() raises).","Run the test that leaks with `pytest -W error::pytest.PytestUnhandledThreadExceptionWarning` to fail fast and locate the source.","Add `threading.excepthook` handling in the app under test so worker exceptions are logged/asserted rather than silently dropped.","Ensure daemon threads are stopped cleanly in fixture teardown (set stop events) before the test ends."],"exampleFix":"// before\nfrom concurrent.futures import ThreadPoolExecutor\ndef test_x():\n    with ThreadPoolExecutor() as ex:\n        ex.submit(lambda: 1/0)  # exception lost, leaked to session\n\n// after\nfrom concurrent.futures import ThreadPoolExecutor\ndef test_x():\n    with ThreadPoolExecutor() as ex:\n        fut = ex.submit(lambda: 1/0)\n        with pytest.raises(ZeroDivisionError):\n            fut.result()","handlingStrategy":"try-catch","validationCode":"import threading, queue\n\ndef run_thread_asserting(target, *args):\n    q: queue.Queue = queue.Queue()\n    def wrapper():\n        try: target(*args)\n        except BaseException as e: q.put(e)\n    t = threading.Thread(target=wrapper); t.start(); t.join()\n    if not q.empty(): raise q.get()","typeGuard":null,"tryCatchPattern":"try:\n    run_test_with_threads()\nexcept* pytest.PytestUnhandledThreadExceptionWarning as eg:\n    for e in eg.exceptions:\n        log_thread_failure(e)","preventionTips":["Always join threads and call future.result() to surface worker exceptions.","Set a custom threading.excepthook in app code that re-raises or records.","Treat PytestUnhandledThreadExceptionWarning as an error in CI (-W error)."],"tags":["threads","exceptiongroup","teardown","concurrency"],"analyzedSha":"98b357f69e380da908740a212288d73b2ee06687","analyzedAt":"2026-08-04T20:26:34.442Z","schemaVersion":2}