{"record":{"id":"448eb3aec771d3ab","repo":"pytest-dev/pytest","slug":"multiple-unraisable-exception-warnings","errorCode":null,"errorMessage":"multiple unraisable exception warnings","messagePattern":"multiple unraisable exception warnings","errorType":"exception","errorClass":"ExceptionGroup","httpStatus":null,"severity":"error","filePath":"src/_pytest/unraisableexception.py","lineNumber":81,"sourceCode":"                continue\n\n            msg = meta.msg\n            try:\n                warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))\n            except pytest.PytestUnraisableExceptionWarning 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 unraisable exception warnings\", errors)\n    finally:\n        del errors, meta, hook_error\n\n\ndef cleanup(\n    *, config: Config, prev_hook: Callable[[sys.UnraisableHookArgs], object]\n) -> None:\n    # On PyPy, objects (e.g. coroutines) can survive GC rounds because executing\n    # their __del__ can resurrect them. The Trio project determined experimentally\n    # that 5 passes are needed on PyPy to flush everything. On CPython, reference\n    # counting handles most cleanup immediately, so 1 pass is sufficient.\n    _default_gc_collect_iterations = 5 if sys.implementation.name == \"pypy\" else 1\n    gc_collect_iterations = config.stash.get(\n        gc_collect_iterations_key, _default_gc_collect_iterations\n    )\n    try:\n        try:\n            gc_collect_harder(gc_collect_iterations)","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/pytest-dev/pytest/blob/0d6fbdeffa57c796123f62f81f7dd370d9b7ecdc/src/_pytest/unraisableexception.py#L63-L99","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before — __del__ raises, pytest groups multiple events\nclass Conn:\n    def __del__(self):\n        self.sock.close()  # may raise if already closed\n\n# after — explicit close, no __del__\nclass Conn:\n    def close(self):\n        try:\n            self.sock.close()\n        except OSError:\n            pass\n\n# in the test\nwith make_conn() as c:\n    ...\nc.close()","handlingStrategy":"try-catch","validationCode":"import gc, sys, pytest\n\ndef assert_no_unraisable():\n    \"\"\"Call at the end of a test to flush and assert no unraisable events.\"\"\"\n    gc.collect()\n    # pytest's hook stashes events; if the stash is empty, we are clean.\n    # Equivalent: rely on filterwarnings to surface them as test failures.","typeGuard":"def has_pending_unraisable(deque) -> bool:\n    return len(deque) > 0","tryCatchPattern":"import pytest\n\ndef test_something_careful():\n    with pytest.MonkeyPatch().context() as mp:\n        # If you expect unraisable noise, filter it; otherwise let it error.\n        mp.setattr(\"warnings.filters\", warnings.filters[:])\n    try:\n        do_work()\n    except BaseException as e:\n        members = getattr(e, \"exceptions\", None)\n        if members and \"unraisable\" in str(e):\n            for m in members:\n                inspect(m.__cause__)\n            raise","preventionTips":["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."],"tags":["unraisable","exceptiongroup","gc","warnings","destructors","pypy"],"backgroundTag":null,"analyzedSha":"0d6fbdeffa57c796123f62f81f7dd370d9b7ecdc","analyzedAt":"2026-08-11T20:52:36.969Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}