skip to content

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

level: middleimportance: should knowfreq 40%

answer

  1. a failing row must name itself
  2. the label comes from what you pass
  3. message bracketed, parameters parenthesised
  4. values rendered with repr, not str
  5. pass nothing and get (<subtest>)

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.

solid answer

~40 s

`subTest` takes an optional positional message and any number of keyword arguments, and both end up in the sub-result's identifier: the message in square brackets, the parameters in parentheses as `name=value` pairs rendered with `repr`. A failure header therefore reads like `test_parse (pkg.QuantityTest.test_parse) [de] (region='de', text='1,5')`, which is enough to reproduce the row without opening the file. Pass the fields that *identify* the row — a case name, the input string — not the whole case object, whose repr turns the report into noise. If you pass nothing at all the label degrades to a bare `(<subtest>)` placeholder, which tells a reader nothing. The `repr` rendering matters for string inputs: it is what distinguishes `'1,5'` from `'1.5'` in the report.

code

python · 22 lines
python
import io
import unittest

CASES = [
    {"region": "us", "text": "1.5", "expected": 1.5},
    {"region": "de", "text": "1,5", "expected": 15.0},
]


class QuantityTest(unittest.TestCase):
    def test_parse(self):
        for case in CASES:
            with self.subTest(region=case["region"], text=case["text"]):
                self.assertEqual(float(case["text"].replace(",", ".")),
                                 case["expected"])


result = unittest.TextTestRunner(stream=io.StringIO()).run(
    unittest.defaultTestLoader.loadTestsFromTestCase(QuantityTest)
)
for failed, _traceback in result.failures:
    print(failed.id())

go deeper

for a junior

Know that the keyword arguments you hand to subTest are what appears in the report, and that omitting them leaves you unable to tell the failing case apart. Write one with a meaningful key.

for a middle

Explain the exact composition: bracketed message, parenthesised parameters, values via repr, appended to the parent test id. Say why repr rather than str is the right choice for string inputs.

for a senior

Show judgement about what belongs in the label versus the table: stable short ids for greppable CI logs, payload kept out of the header, and the point at which generated methods beat a labelled loop.

for a principal

Own the reporting contract with your CI tooling — what a failure line must contain for someone paged at night to reproduce a row without a local checkout, and whether that argues for generated methods across the suite.

## The label is the whole value of the feature Per-case reporting is only useful if a reader can tell *which* case each entry refers to. `unittest.TestCase.subTest` builds that label from what you pass it, and passing nothing is the most common way to waste the feature. The signature takes an optional positional message first, then arbitrary keyword parameters: ```python with self.subTest("locale row", region="de", text="1,5"): ... ``` The sub-result's description is assembled as: the message in square brackets when given, then the parameters in parentheses as comma-separated `name=value` pairs, with each value rendered by `repr`. The full identifier is the parent test's id followed by that description, so a failing entry prints as something like: ``` FAIL: test_parse (pkg.tests.QuantityTest.test_parse) [locale row] (region='de', text='1,5') ``` When you pass neither a message nor parameters, the description collapses to the literal placeholder `(<subtest>)` — every failing case in the loop then produces an identical-looking header, and you are back to reading tracebacks to work out which row broke. ## Why repr, and why it matters Values are formatted with `repr`, not `str`. That is deliberate: it is what makes `'1,5'` visibly different from `'1.5'`, makes an empty string visible at all, and distinguishes `1` from `'1'` and from `True`. Anywhere the difference between two inputs is a delimiter, a trailing space, or a `str`/`bytes` distinction, `str` formatting would render the two rows identically and the label would lie. The flip side is that `repr` of a large object is a wall of text in the middle of the report. Pass identity, not payload: ```python for case in CASES: with self.subTest(case_id=case["id"]): check(case) # the whole case dict stays out of the label ``` ## Building the case table as data The label question and the table question are the same question, because the label is drawn from the table's identifying columns. A workable shape is a module-level or class-level sequence of dicts or `namedtuple`s where one field is a stable, human-meaningful id: ```python Row = collections.namedtuple("Row", "id text expected") ROWS = [ Row("plain", "1.5", 1.5), Row("comma", "1,5", 1.5), ] class ParseTest(unittest.TestCase): def test_rows(self): for row in ROWS: with self.subTest(row=row.id): self.assertEqual(parse_qty(row.text), row.expected) ``` Three properties make this table hold up in review. The ids are stable, so a failure in CI logs is greppable and a flaky row can be named in a ticket. The loop body is one assertion, so the reader sees the *rule* being checked once rather than repeated per row. And the table is ordinary data, so it can be extended by someone who has never read the test framework's documentation — which is usually the point of writing it this way. `self.subTest(**case)` is tempting when the table is a dict of small scalars, and it is fine there; it stops being fine as soon as one column is an object, a long fixture string, or a value that is expensive to `repr`. ## The case label and an assertion message are different strings Two strings compete for the same job and land in different places. The positional argument to `subTest` labels the *case*: it appears once in the sub-result's header, whether the case ended in an assertion failure or an unexpected exception. The `msg=` keyword accepted by the assertion helpers labels the *assertion*: it appears inside the failure body, appended to the standard diff. A loop that checks three properties per row usually wants both — the row id on the `subTest`, and a short `msg=` on each assertion naming which property broke. Passing the row id to every assertion instead leaves the header anonymous and repeats the same string three times in the body. ## What the label does not do The label is a display string, not an addressable name. You cannot rerun one case by dotted path the way you can rerun a test method, because there is no method to name — the selection unit is still the method. If a particular row must be individually runnable, skippable, or excludable from CI, that is the signal to generate real test methods from the table instead, at the cost of a factory a reader has to decode. And note the message is a *label*, not an assertion message: the `msg=` argument you pass to an assertion helper is separate and still appears in the assertion's own output. ## Interviewer's angle The question separates people who have merely seen the idiom from people who have read a failing report at three in the morning. Mentioning `repr` rendering and the `(<subtest>)` placeholder is the concrete detail that shows the second.

  • What appears in the report if you call subTest with no message and no keyword arguments?
    The description collapses to the literal placeholder `(<subtest>)`. Every failing case in the loop then produces an indistinguishable header, so the only way to identify the row is to read each traceback's line number and values. It is the single most common way teams get the mechanics of subTest right and the value of it wrong.
  • Why pass the row's id rather than the whole case object as a parameter?
    Parameters are rendered with `repr`, so a large dict, a fixture blob or an object with a verbose repr turns every failure header into a wall of text — and a repr that includes a memory address changes between runs, which breaks grep and log diffing. Pass a short stable identifier and keep the payload in the table.
  • Can you rerun a single failing subtest from the command line?
    No. The label is display text, not an addressable name; unittest's selection unit is still the test method, so you rerun the method and all of its cases. If one row genuinely needs to be run, skipped or excluded on its own, generate a real test method per row from the table instead.

saying these in an interview costs you the question

  • Thinks the label appears automatically without arguments
  • Passes the whole case object and floods the report
  • Believes values are formatted with str, not repr
  • Says a single subtest can be rerun by dotted path
  • Confuses the subTest message with an assertion msg argument

context