pytest-dev/pytest · error · ExceptionGroup
Unittest class cleanup errors
Error message
Unittest class cleanup errors
What it means
For unittest.TestCase classes, pytest registers a class-scope fixture that runs setUpClass/tearDownClass and then calls doClassCleanups(). unittest stashes any cleanup failures as exc_infos in cls.tearDown_exceptions. process_teardown_exceptions() drains that list: a single failure is re-raised as-is for readability, but two or more are wrapped in an ExceptionGroup so every cleanup error is reported rather than just the first. This matches unittest's own behavior but surfaces the errors through pytest's outcome handling.
Solutions
- Read each member of the ExceptionGroup — each is one cleanup failure; fix them individually rather than chasing only the top of the group.
- Make cleanups independent and defensive so one failing cleanup does not cascade into others (wrap each in try/except and aggregate yourself if you need custom handling).
- Prefer pytest fixtures with yield teardowns over unittest addClassCleanup when mixing the two, so teardown failures surface one at a time per fixture.
- Run the class in isolation (`pytest path/to/test.py::TestClass`) to confirm the group is class-scoped, not from another test.
Example fix
# before — two cleanups, both fail, pytest groups them
class TestX(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.addClassCleanup(cls.close_a)
cls.addClassCleanup(cls.close_b)
# after — each cleanup self-isolates
class TestX(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.addClassCleanup(cls._safe, cls.close_a)
cls.addClassCleanup(cls._safe, cls.close_b)
@classmethod
def _safe(cls, fn):
try:
fn()
except Exception as e:
print("cleanup failed:", e) Defensive patterns
Strategy: try-catch
Validate before calling
import unittest
def run_class_cleanups_safely(cls: type):
"""Invoke doClassCleanups and surface a list of errors instead of a group."""
cls.doClassCleanups()
infos = getattr(cls, "tearDown_exceptions", []) or []
return [exc for (_, exc, _) in infos] Type guard
def unittest_class_has_cleanup_errors(cls: type) -> bool:
infos = getattr(cls, "tearDown_exceptions", None)
return bool(infos) Try / catch
import unittest
from exceptiongroup import ExceptionGroup as _EG # py<3.11
def assert_class_cleanups_ok(testcase: unittest.TestCase):
try:
testcase.doClassCleanups()
except BaseException as e:
members = getattr(e, "exceptions", None)
if members:
raise AssertionError([str(m) for m in members]) from e
raise Prevention
- Make each addClassCleanup callback independent and self-defensive so one failure does not cascade.
- Prefer pytest fixture teardowns (yield) over unittest addClassCleanup when mixing frameworks.
- Run the class in isolation to confirm the group originates from class-scope teardown.
When it happens
Trigger: A unittest.TestCase with addClassCleanup(...) callbacks where two or more cleanups raise during doClassCleanups; a tearDownClass that fails AND a class-cleanup that also fails; multiple addCleanup (instance-level) failures accumulating within class teardown.
Common situations: Complex unittest suites migrated to pytest where teardowns open files/connections/sockets and several fail together; a fixture-like cleanup chain where an early failure leaves state that breaks later cleanups; tearDownClass raising after a setup failure already populated tearDown_exceptions.
Related errors
- errors while tearing down fixture
- errors during test teardown
- multiple thread exception warnings
- multiple unraisable exception warnings
- The fixture value for
AI-assisted analysis of pytest-dev/pytest@0d6fbdeffa (2026-08-11).
Data as JSON: /api/errors/7c105812f8a95e0d.
Report an issue: GitHub.
Appendix: source
Thrown at src/_pytest/unittest.py:148
setup = getattr(cls, "setUpClass", None)
teardown = getattr(cls, "tearDownClass", None)
if setup is None and teardown is None:
return None
cleanup = getattr(cls, "doClassCleanups", lambda: None)
def process_teardown_exceptions() -> None:
# tearDown_exceptions is a list set in the class containing exc_infos for errors during
# teardown for the class.
exc_infos = getattr(cls, "tearDown_exceptions", None)
if not exc_infos:
return
exceptions = [exc for (_, exc, _) in exc_infos]
# If a single exception, raise it directly as this provides a more readable
# error (hopefully this will improve in #12255).
if len(exceptions) == 1:
raise exceptions[0]
else:
raise ExceptionGroup("Unittest class cleanup errors", exceptions)
def unittest_setup_class_fixture(
request: FixtureRequest,
) -> Generator[None]:
cls = request.cls
if _is_skipped(cls):
reason = cls.__unittest_skip_why__
raise skip.Exception(reason, _use_item_location=True)
if setup is not None:
try:
setup()
# unittest does not call the cleanup function for every BaseException, so we
# follow this here.
except Exception:
cleanup()
process_teardown_exceptions()
raise
yieldView on GitHub (pinned to 0d6fbdeffa)