pytest-dev/pytest · error · ExceptionGroup
multiple unraisable exception warnings
Error message
multiple unraisable exception warnings
What it means
pytest installs sys.unraisablehook to capture exceptions Python could not propagate — typically from __del__ methods, GC-time finalizers, or C-extension callbacks. Each captured event becomes a PytestUnraisableExceptionWarning; at each test-phase boundary and at session unconfigure, collect_unraisable() drains the queue. Two or more captured warnings promoted to errors (via -Werror or filterwarnings=error) are wrapped in an ExceptionGroup so none are silently dropped. One is re-raised directly.
Solutions
- Inspect each ExceptionGroup member — its __cause__ is the original unraisable exception; fix the __del__ or finalizer at the source.
- Replace __del__ with explicit context managers (with statements) or pytest fixture teardowns so failures become normal test errors, not unraisable ones.
- Add a targeted filterwarnings entry (e.g. `ignore::pytest.PytestUnraisableExceptionWarning`) if the unraisable events are expected noise.
- Force deterministic cleanup (gc.collect() inside the test, close sockets/files explicitly) so the unraisable never fires during pytest's hooks.
Example fix
# before — __del__ raises, pytest groups multiple events
class Conn:
def __del__(self):
self.sock.close() # may raise if already closed
# after — explicit close, no __del__
class Conn:
def close(self):
try:
self.sock.close()
except OSError:
pass
# in the test
with make_conn() as c:
...
c.close() Defensive patterns
Strategy: try-catch
Validate before calling
import gc, sys, pytest
def assert_no_unraisable():
"""Call at the end of a test to flush and assert no unraisable events."""
gc.collect()
# pytest's hook stashes events; if the stash is empty, we are clean.
# Equivalent: rely on filterwarnings to surface them as test failures. Type guard
def has_pending_unraisable(deque) -> bool:
return len(deque) > 0 Try / catch
import pytest
def test_something_careful():
with pytest.MonkeyPatch().context() as mp:
# If you expect unraisable noise, filter it; otherwise let it error.
mp.setattr("warnings.filters", warnings.filters[:])
try:
do_work()
except BaseException as e:
members = getattr(e, "exceptions", None)
if members and "unraisable" in str(e):
for m in members:
inspect(m.__cause__)
raise Prevention
- Avoid __del__; use context managers or pytest fixture teardowns for cleanup.
- Close sockets/files/loops explicitly inside the test rather than relying on GC.
- Add a targeted filterwarnings ignore rule if unraisable events are expected and benign.
- On PyPy, call gc.collect() inside the test to make finalizer errors deterministic.
When it happens
Trigger: Two or more objects raise inside __del__ during a single test phase; a C extension logs unraisable errors twice; coroutines/sockets/files whose finalizers each fail during GC; running with filterwarnings=error so the otherwise-warned unraisable events turn into errors and aggregate.
Common situations: Tests that construct objects with buggy __del__; asyncio loops not closed cleanly so multiple coroutine warnings fire; C-extension leaks surfacing as unraisable; PyPy where multi-pass GC resurrects objects and produces several events; CI with -Werror turning the warnings into a group.
Related errors
- multiple thread exception warnings
- is not a Warning subclass
- errors while tearing down fixture
- invalid lineno
- Invalid regex
AI-assisted analysis of pytest-dev/pytest@0d6fbdeffa (2026-08-11).
Data as JSON: /api/errors/448eb3aec771d3ab.
Report an issue: GitHub.
Appendix: source
Thrown at src/_pytest/unraisableexception.py:81
continue
msg = meta.msg
try:
warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
except pytest.PytestUnraisableExceptionWarning 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 unraisable exception warnings", errors)
finally:
del errors, meta, hook_error
def cleanup(
*, config: Config, prev_hook: Callable[[sys.UnraisableHookArgs], object]
) -> None:
# On PyPy, objects (e.g. coroutines) can survive GC rounds because executing
# their __del__ can resurrect them. The Trio project determined experimentally
# that 5 passes are needed on PyPy to flush everything. On CPython, reference
# counting handles most cleanup immediately, so 1 pass is sufficient.
_default_gc_collect_iterations = 5 if sys.implementation.name == "pypy" else 1
gc_collect_iterations = config.stash.get(
gc_collect_iterations_key, _default_gc_collect_iterations
)
try:
try:
gc_collect_harder(gc_collect_iterations)View on GitHub (pinned to 0d6fbdeffa)