skip to content

Why does self.addCleanup still run when a unittest setUp raises halfway through?

level: middleimportance: should knowfreq 46%

answer

  1. A half-finished setup still owns resources
  2. Register the release, do not defer it
  3. A stack, drained in reverse
  4. Runs after tearDown, and after a crash
  5. Since 3.11 there is a with-shaped form

basics

~10 s

addCleanup registers a callable the moment you call it, and unittest drains that stack after every test - even one whose setUp died partway. tearDown, by contrast, is skipped entirely when setUp raises.

solid answer

~40 s

`self.addCleanup(func, *args)` records the callable **at registration time** rather than deferring the whole teardown to one function. unittest drains that stack after the test method and after `tearDown`, in **LIFO** order, and it drains it whatever happened - pass, failure, error, or a `setUp` that never finished. `tearDown` cannot match that: when `setUp` raises, unittest skips both the test method and `tearDown`, so anything acquired before the exception leaks unless its release was already registered. Practically this means pairing each acquisition with its cleanup on the spot, which also removes the `hasattr` guards a defensive `tearDown` accumulates and survives inheritance without anyone calling `super().tearDown()`. Since 3.11, `self.enterContext(cm)` does the same for a synchronous context manager: it enters it, registers `__exit__` as a cleanup, and returns the entered value.

code

python · 17 lines
python
import unittest


class PartialSetUpTests(unittest.TestCase):
    def setUp(self):
        self.addCleanup(print, "unregister collector")
        self.addCleanup(print, "close socket")
        raise RuntimeError("cold start timed out")

    def tearDown(self):
        print("tearDown is never reached")

    def test_scrape(self):
        pass


unittest.main(verbosity=0, exit=False)

go deeper

for a junior

Know that addCleanup exists and that it is the safer place to put teardown work. Remember to pass the function and its arguments separately rather than calling it, which is the mistake that bites first.

for a middle

Explain registration-time semantics and LIFO draining, and why a setUp that raises skips tearDown but not the cleanups already registered. Be ready to rewrite a guard-heavy tearDown as paired acquisitions and cleanups.

for a senior

Show how this prevents cross-test contamination in a real suite: a resource left registered by a half-failed setup produces duplicated side effects in a later, unrelated test. Reach for enterContext and addClassCleanup rather than defensive teardown code.

for a principal

Own the convention across the codebase: every acquisition registers its release on the same line, teardown is unconditional by construction, and base-class fixtures never depend on subclasses calling super(). That is what keeps a large suite's failures attributable.

### The problem `tearDown` cannot solve `setUp` is one function that often does several things: open a connection, register a collector with a process-wide registry, patch an attribute, seed a table. If it raises on the third step, unittest reports the test as an **error** and **does not call `tearDown`** - so whatever the first two steps acquired is still live when the next test starts. That is how a suite develops a duplicated side effect: a metrics scraper whose collector was registered but never unregistered leaves a second collector behind, and from then on every sample is emitted twice, in a test that looks entirely unrelated to the one that actually broke. `tearDown` cannot fix this on its own, because it is one function that has to guess how far `setUp` got. The usual patch - `if getattr(self, "conn", None): self.conn.close()` - is guesswork that grows a branch per resource. ### How `addCleanup` works `self.addCleanup(func, *args, **kwargs)` records a callable and its arguments on the instance **at the moment you call it**. It runs nothing then. After the test method and after `tearDown`, unittest drains that stack in **LIFO** order, so resources are released in the reverse of the order they were acquired - the same discipline as nested `with` blocks or a `contextlib.ExitStack`. The stack is drained *whatever happened*: the test passed, failed, errored, or `setUp` never finished. Registration is the commitment; nothing about it is conditional on reaching the test body. If a cleanup itself raises, that exception is recorded against the test and the remaining cleanups still run - one bad release does not strand the rest. `self.doCleanups()` drains the stack early and reports whether every entry succeeded. You rarely need it directly; it exists for custom runners and for a `setUp` that wants to unwind explicitly. ### `enterContext`, added in 3.11 `self.enterContext(cm)` calls the synchronous context manager's `__enter__()`, registers its `__exit__` as a cleanup, and returns whatever `__enter__` produced. It is `addCleanup` for anything that is already a `with`-shaped object: ```python def setUp(self): self.session = self.enterContext(scraper_session()) ``` That single line acquires and guarantees release, with no `tearDown` and no `try`. (An async test case has its own async equivalents; the manager `enterContext` takes is the synchronous `__enter__`/`__exit__` kind.) ### The class-scoped counterparts `addClassCleanup(func, *args)` arrived in 3.8 as the class method counterpart: register it inside `setUpClass`, and it runs once after `tearDownClass`, LIFO. Its real value is the same asymmetry one scope up - **class cleanups run even when `setUpClass` raises**, where `tearDownClass` does not. `doClassCleanups()` drains that stack explicitly, and `enterClassContext` (3.11) is the class-scoped `enterContext`. ### The full ordering, once Per test: `setUp` -> the `test_*` method -> `tearDown` -> instance cleanups, last registered first. Per class: `setUpClass` -> all the class's tests -> `tearDownClass` -> class cleanups, last registered first. ### Why this is the better default - **Registration sits at the point of acquisition.** There is no window between "I created it" and "someone must remember to destroy it", and no second function to keep in sync when you add a resource. - **A partial `setUp` is safe.** Whatever was acquired is released; whatever was not was never registered. - **It is inheritance-safe.** A base class's cleanups do not depend on a subclass remembering `super().tearDown()`. - **Conditional resources need no guards.** A resource acquired only on one branch registers its cleanup only on that branch, so `tearDown` never needs a `hasattr` check. ### The failure modes to name - `self.addCleanup(fn())` - calling the function instead of passing it. That registers its *return value*; if it returned `None` you get a `TypeError` at cleanup time, long after the real mistake. - Assuming cleanups run in registration order. They run in reverse, and a dependency chain released forwards will fail. - Assuming one raising cleanup cancels the rest. It does not. - Believing `addCleanup` replaces `tearDown` and runs *before* it. Cleanups run after `tearDown`, so a `tearDown` that reads a resource is reading one that is still alive. - Registering a `lambda` that closes over a loop variable and captures its final value. The rule that follows from all of it: **acquire and register on the same line, and let `tearDown` exist only for state you genuinely cannot pair with its acquisition.**

  • In what order do registered cleanups run, and what happens if one of them raises?
    They run last-registered-first, so releases mirror acquisitions the way nested `with` blocks do. If a cleanup raises, unittest records that exception against the test and continues draining the rest of the stack - one failed release never strands the others. That is deliberate: teardown is exactly where you least want an early exit.
  • What does TestCase.addClassCleanup give you that tearDownClass does not?
    The same asymmetry one scope up. `addClassCleanup`, added in 3.8, registers a class-scoped callable that runs after `tearDownClass` in LIFO order - and crucially it still runs when `setUpClass` itself raises, where `tearDownClass` does not. An expensive class-scoped resource should therefore register its release with `addClassCleanup` rather than trust `tearDownClass`.
  • What is wrong with writing self.addCleanup(close_session())?
    It calls `close_session()` immediately and registers whatever it returned. The session closes at setup time instead of teardown, and the registered object is usually `None`, so the cleanup phase raises `TypeError: 'NoneType' object is not callable` long after the real mistake. Pass the function and its arguments separately: `self.addCleanup(close_session)`, or `self.addCleanup(session.close)`.

addCleanup is the finally you write the moment you open the door, rather than the one you hope somebody remembers to add at the bottom of the function.

saying these in an interview costs you the question

  • Thinks cleanups run in registration order, not reverse
  • Calls the function instead of passing it to addCleanup
  • Believes tearDown covers a setUp that failed halfway
  • Assumes one raising cleanup cancels the remaining ones
  • Says addCleanup replaces tearDown and runs before it
  • Registers a lambda that captures a loop variable late

context