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

  1. Read each member of the ExceptionGroup — each is one cleanup failure; fix them individually rather than chasing only the top of the group.
  2. 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).
  3. Prefer pytest fixtures with yield teardowns over unittest addClassCleanup when mixing the two, so teardown failures surface one at a time per fixture.
  4. 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

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


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
            yield

View on GitHub (pinned to 0d6fbdeffa)