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

  1. Join threads and propagate their exceptions: capture via a queue/future and re-raise in the main thread (e.g. futures.result() raises).
  2. Run the test that leaks with `pytest -W error::pytest.PytestUnhandledThreadExceptionWarning` to fail fast and locate the source.
  3. Add `threading.excepthook` handling in the app under test so worker exceptions are logged/asserted rather than silently dropped.
  4. 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

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


AI-assisted analysis of pytest-dev/pytest@98b357f69e (2026-08-04). Data as JSON: /data/errors/62827611aec0a756.json. Report an issue: GitHub.