pytest-dev/pytest · warning · ExceptionGroup
multiple thread exception warnings
Error message
multiple thread exception warnings
What it means
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.
Source
Thrown at src/_pytest/threadexception.py:72
continue
msg = meta.msg
try:
warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg))
except pytest.PytestUnhandledThreadExceptionWarning as e:
# This except happens when the warning is treated as an error (e.g. `-Werror`).
if meta.exc_value is not None:
# Exceptions have a better way to show the traceback, but
# warnings do not, so hide the traceback from the msg and
# set the cause so the traceback shows up in the right place.
e.args = (meta.cause_msg,)
e.__cause__ = meta.exc_value
errors.append(e)
if len(errors) == 1:
raise errors[0]
if errors:
raise ExceptionGroup("multiple thread exception warnings", errors)
finally:
del errors, meta, hook_error
def cleanup(
*, config: Config, prev_hook: Callable[[threading.ExceptHookArgs], object]
) -> None:
try:
try:
# We don't join threads here, so exceptions raised from any
# threads still running by the time _threading_atexits joins them
# do not get captured (see #13027).
collect_thread_exception(config)
finally:
threading.excepthook = prev_hook
finally:
del config.stash[thread_exceptions]
View on GitHub (pinned to 98b357f69e)
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.
Example fix
// before
from concurrent.futures import ThreadPoolExecutor
def test_x():
with ThreadPoolExecutor() as ex:
ex.submit(lambda: 1/0) # exception lost, leaked to session
// after
from concurrent.futures import ThreadPoolExecutor
def test_x():
with ThreadPoolExecutor() as ex:
fut = ex.submit(lambda: 1/0)
with pytest.raises(ZeroDivisionError):
fut.result() Defensive patterns
Strategy: try-catch
Validate before calling
import threading, queue
def run_thread_asserting(target, *args):
q: queue.Queue = queue.Queue()
def wrapper():
try: target(*args)
except BaseException as e: q.put(e)
t = threading.Thread(target=wrapper); t.start(); t.join()
if not q.empty(): raise q.get() Try / catch
try:
run_test_with_threads()
except* pytest.PytestUnhandledThreadExceptionWarning as eg:
for e in eg.exceptions:
log_thread_failure(e) Prevention
- 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).
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- errors while tearing down fixture "{self.argname}" of {node}
- errors during test teardown
- Unittest class cleanup errors
- multiple unraisable exception warnings
- The fixture value for "{argname}" is not available during te
AI-assisted analysis of pytest-dev/pytest@98b357f69e (2026-08-04).
Data as JSON: /data/errors/62827611aec0a756.json.
Report an issue: GitHub.