skip to content

subTest and Parametrization

Running the same assertions over many inputs without a method per case: subTest reports each case independently. A favorite question because the naive for-loop hides every failure after the first.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does unittest's TestCase.subTest do inside a loop of test cases?

level: juniorimportance: must knowfreq 50%

answer

  1. one method, many inputs, one verdict
  2. the first failure ends the method
  3. a guard around each iteration
  4. every bad case reported in one run
  5. with self.subTest(value=...)

basics

~20 s

unittest.TestCase.subTest is a context manager that wraps one iteration of a loop, so a failure inside it is recorded and the loop keeps going instead of the first failed assertion ending the whole test method.

solid answer

~40 s

A plain `for` loop inside one test method shares a single failure path: the first failed assertion raises `AssertionError`, unittest records one failure against the method, and every later case never runs — so you fix one row, rerun, and discover the next. Wrapping each iteration in `with self.subTest(...)` turns each case into its own reported unit: the context manager catches whatever the block raises, hands it to the result object through `addSubTest`, and returns normally so the loop continues. One run then reports every bad case, each labelled with the keyword arguments you passed. The method still counts as one test in the run total; what grows is the failure list. It has been in the standard library since Python 3.4 and needs no third-party runner.

code

python · 27 lines
python
import io
import unittest

CASES = [("1.5", 1.5), ("1,5", 15.0), ("2.5", 25.0)]


def parse_qty(text):
    return float(text.replace(",", "."))


class NaiveTest(unittest.TestCase):
    def test_all(self):
        for text, expected in CASES:
            self.assertEqual(parse_qty(text), expected)


class SubTestTest(unittest.TestCase):
    def test_all(self):
        for text, expected in CASES:
            with self.subTest(text=text):
                self.assertEqual(parse_qty(text), expected)


load = unittest.defaultTestLoader.loadTestsFromTestCase
for case in (NaiveTest, SubTestTest):
    result = unittest.TextTestRunner(stream=io.StringIO()).run(load(case))
    print(case.__name__, result.testsRun, len(result.failures))

go deeper

for a junior

Be ready to say what happens to the rest of a loop when an assertion fails, and to write the four-line with self.subTest(...) idiom from memory. Knowing the failure ends the method is the point of the question.

for a middle

Explain the mechanics: the context manager catches the exception, reports it through the result object, and lets the loop continue. Be precise that assertions become failures and other exceptions become errors, per case.

for a senior

Show where the boundary stops. subTest gives per-case reporting, not per-case fixtures, and code outside the with block is unguarded — call that out before an interviewer has to.

for a principal

Own the choice between a data table with subTest and generated test methods for a whole suite: readability of the case table, isolation guarantees, and what CI reporting your team actually consumes.

## The failure path a plain loop gives you A `unittest.TestCase` method is one unit of reporting. When an assertion inside it fails, the assertion helper raises `AssertionError`; that exception unwinds out of the method, unittest's outcome machinery catches it, records one failure against the method's id, and moves on to the next method. **Nothing after the raise executes.** That is exactly right when the method tests one thing, and exactly wrong when the method loops over a table of inputs — the first bad row masks every row behind it, so a twenty-row table with four bugs costs four full edit-and-rerun cycles to drain. The other reflex fix — collecting mismatches into a list and asserting the list is empty at the end — restores the coverage but destroys the diagnosis. You get one `AssertionError` whose message you had to format by hand, no per-case traceback, and nothing the runner can count. ## What subTest actually does `TestCase.subTest` is a context manager, in the standard library since Python 3.4 and semantically unchanged through 3.14. Around its body it reuses the same execution machinery unittest applies to a test method, but tagged as a *sub*test: - it builds a lightweight test-like object carrying the parent test's id plus the keyword arguments you passed; - it runs the body in an executor that catches everything except `KeyboardInterrupt`; - if the body raised, it passes the exception to the result object's `addSubTest` and then exits the `with` normally, so the loop's next iteration starts; - if the body passed, `addSubTest` is still called with no exception, which is how a verbose runner can account for successful cases. The load-bearing consequence is the third bullet: control comes back to the loop after a failure, so a single run reports every bad case at once. ```python CASES = [("1.5", 1.5), ("1,5", 15.0), ("2.5", 25.0)] class ParseTest(unittest.TestCase): def test_all(self): for text, expected in CASES: with self.subTest(text=text): self.assertEqual(parse_qty(text), expected) ``` ## Failure versus error, per case Classification follows the exception type, not the wrapper. If the block raises the test case's `failureException` — `AssertionError` — the case is recorded as a **failure**; any other exception is recorded as an **error** for that case. Either way the loop continues. That surprises people who assume `subTest` only guards assertions: a row that makes the code under test raise `ValueError` is reported against that row and the remaining rows still run. ## What it does not change Three things stay exactly as they were: 1. **The run total counts methods.** A method with fifty cases still prints `Ran 1 test`. Only the failure and error lists grow, and they can hold several entries pointing at the same method. 2. **Fixtures run once.** `setUp` and `tearDown` bracket the whole method, not each case; `addCleanup` callbacks fire after the method finishes. `subTest` is a reporting boundary, not an isolation boundary. 3. **The method's own code is not protected.** Only what is lexically inside the `with` block is guarded; an exception in the loop header or between cases still aborts the method. ## Building the table as data The idiom pairs naturally with a case table declared as data — a list of tuples, dicts or `namedtuple`s at module level or in a class attribute — because the loop body then stays a single assertion and the table is the thing reviewers read. Pass the identifying fields as keyword arguments so the report can name the row, and keep any per-case setup inside the `with` block so a failure cannot half-mutate state the next row reads. ## When to reach for something else `subTest` is the right tool when the same assertion runs over varying data. It is the wrong tool when each case needs its own fixture (build real methods from the table instead, so `setUp` re-runs), or when the cases are genuinely different behaviours rather than different inputs — those deserve separate methods with names that say what they check, because a name is the cheapest documentation a suite has. Other runners offer a decorator that generates one test per row; inside the standard library, `subTest` is the mechanism you have, and it is enough.

  • Does the run summary count each subtest as a separate test?
    No. `Ran N tests` counts test methods, so a method with fifty cases still counts as one. What multiplies is the failure and error lists: each bad case appears as its own entry with its own traceback and label. Anyone using the tests-run number as a measure of how many inputs were exercised will read it wrong.
  • Does subTest catch only AssertionError, or any exception raised in the block?
    Any exception except `KeyboardInterrupt`. An `AssertionError` is recorded as a failure for that case; anything else — a `ValueError` out of the code under test, say — is recorded as an error for that case. Both leave the loop running. Only code outside the `with` block, such as the loop header itself, can still abort the whole method.
  • Why is asserting a collected list of mismatches at the end a worse fix?
    It restores coverage but loses the diagnosis. You get one `AssertionError` at the end with a message you formatted by hand, no per-case traceback pointing at the failing assertion, and no per-case entry the runner can report or a CI tool can parse. `subTest` gives the runner structured per-case results for free.

A plain loop is a fuse box where the first blown fuse kills the whole circuit; subTest gives each case its own breaker, so one trip is logged and the rest of the house stays lit.

saying these in an interview costs you the question

  • Says a plain loop keeps going after an assertion fails
  • Thinks subTest generates a separate test method per case
  • Believes subTest intercepts only AssertionError
  • Says each subtest increments the tests-run total
  • Claims looping over cases needs a third-party runner
  • Assumes setUp re-runs before every subtest

context

open as a page

In unittest, how does TestCase.subTest label which case failed?

level: middleimportance: should knowfreq 40%

basics

~10 s

Every keyword argument passed to unittest's TestCase.subTest, plus an optional leading message, is echoed in the failure header beside the test id, so each entry names the exact case instead of only the method.

open as a page

Why can a unittest TestCase.subTest loop leak state between its cases?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Because subTest is a reporting boundary, not an isolation boundary: setUp, tearDown and cleanup callbacks bracket the whole test method, so anything a case mutates — instance attributes, module globals, caches, the filesystem — is still there for the next case.

open as a page

Why can a failure inside unittest's TestCase.subTest still abort the loop?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

Three escapes exist: code outside the with block is unguarded, fail-fast mode stops the method at the first bad case, and a result object without an addSubTest method makes subTest degrade to a plain pass-through so the first failure propagates.

open as a page