{"record":{"id":"7c105812f8a95e0d","repo":"pytest-dev/pytest","slug":"unittest-class-cleanup-errors","errorCode":null,"errorMessage":"Unittest class cleanup errors","messagePattern":"Unittest class cleanup errors","errorType":"exception","errorClass":"ExceptionGroup","httpStatus":null,"severity":"error","filePath":"src/_pytest/unittest.py","lineNumber":148,"sourceCode":"        setup = getattr(cls, \"setUpClass\", None)\n        teardown = getattr(cls, \"tearDownClass\", None)\n        if setup is None and teardown is None:\n            return None\n        cleanup = getattr(cls, \"doClassCleanups\", lambda: None)\n\n        def process_teardown_exceptions() -> None:\n            # tearDown_exceptions is a list set in the class containing exc_infos for errors during\n            # teardown for the class.\n            exc_infos = getattr(cls, \"tearDown_exceptions\", None)\n            if not exc_infos:\n                return\n            exceptions = [exc for (_, exc, _) in exc_infos]\n            # If a single exception, raise it directly as this provides a more readable\n            # error (hopefully this will improve in #12255).\n            if len(exceptions) == 1:\n                raise exceptions[0]\n            else:\n                raise ExceptionGroup(\"Unittest class cleanup errors\", exceptions)\n\n        def unittest_setup_class_fixture(\n            request: FixtureRequest,\n        ) -> Generator[None]:\n            cls = request.cls\n            if _is_skipped(cls):\n                reason = cls.__unittest_skip_why__\n                raise skip.Exception(reason, _use_item_location=True)\n            if setup is not None:\n                try:\n                    setup()\n                # unittest does not call the cleanup function for every BaseException, so we\n                # follow this here.\n                except Exception:\n                    cleanup()\n                    process_teardown_exceptions()\n                    raise\n            yield","sourceCodeStart":130,"sourceCodeEnd":166,"githubUrl":"https://github.com/pytest-dev/pytest/blob/0d6fbdeffa57c796123f62f81f7dd370d9b7ecdc/src/_pytest/unittest.py#L130-L166","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before — two cleanups, both fail, pytest groups them\nclass TestX(unittest.TestCase):\n    @classmethod\n    def setUpClass(cls):\n        cls.addClassCleanup(cls.close_a)\n        cls.addClassCleanup(cls.close_b)\n\n# after — each cleanup self-isolates\nclass TestX(unittest.TestCase):\n    @classmethod\n    def setUpClass(cls):\n        cls.addClassCleanup(cls._safe, cls.close_a)\n        cls.addClassCleanup(cls._safe, cls.close_b)\n    @classmethod\n    def _safe(cls, fn):\n        try:\n            fn()\n        except Exception as e:\n            print(\"cleanup failed:\", e)","handlingStrategy":"try-catch","validationCode":"import unittest\n\ndef run_class_cleanups_safely(cls: type):\n    \"\"\"Invoke doClassCleanups and surface a list of errors instead of a group.\"\"\"\n    cls.doClassCleanups()\n    infos = getattr(cls, \"tearDown_exceptions\", []) or []\n    return [exc for (_, exc, _) in infos]","typeGuard":"def unittest_class_has_cleanup_errors(cls: type) -> bool:\n    infos = getattr(cls, \"tearDown_exceptions\", None)\n    return bool(infos)","tryCatchPattern":"import unittest\nfrom exceptiongroup import ExceptionGroup as _EG  # py<3.11\n\ndef assert_class_cleanups_ok(testcase: unittest.TestCase):\n    try:\n        testcase.doClassCleanups()\n    except BaseException as e:\n        members = getattr(e, \"exceptions\", None)\n        if members:\n            raise AssertionError([str(m) for m in members]) from e\n        raise","preventionTips":["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."],"tags":["unittest","exceptiongroup","teardown","cleanup","class-scope"],"backgroundTag":null,"analyzedSha":"0d6fbdeffa57c796123f62f81f7dd370d9b7ecdc","analyzedAt":"2026-08-11T20:52:36.969Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}