Testing Code That Raises ExceptionGroups¶
Code that uses TaskGroup fails with an ExceptionGroup, and tests written for the old single-exception world quietly stop working. Tested with pytest 9.1: pytest.raises(ValueError) around a TaskGroup whose task raised ValueError failed — pytest reported "ValueError('x') [single exception in ExceptionGroup]". Checking the group with excinfo.group_contains(ValueError, match="row 3") passed and allowed extra members; pytest.RaisesGroup(ValueError) was strict and failed on an unexpected KeyError in the group, which is exactly the regression an assertion should catch. RaisesGroup also matched nested groups with flatten_subgroups=True and a bare exception with allow_unwrapped=True. This guide picks the right assertion for each test.
Prerequisites¶
- Python 3.11+, pytest 8.4+ for
RaisesGroup(tested with pytest 9.1.1), pytest-asyncio for async tests. - Groups from TaskGroup, from handling ExceptionGroup from TaskGroup.
- Async test setup, from testing asyncio code with pytest-asyncio.
1. Replace pytest.raises(SomeError) around TaskGroups¶
A TaskGroup raises a group even for one failure, so the old assertion fails:
async def batch(*excs):
async with asyncio.TaskGroup() as tg:
for exc in excs:
tg.create_task(fail(exc))
async def test_old_style():
with pytest.raises(ValueError): # FAILS: ValueError('x') [single exception in ExceptionGroup]
await batch(ValueError("x"))
Tested: pytest reported the failure helpfully, naming the single exception inside the group. This is the first thing to fix when migrating code from gather to TaskGroup, as in migrating from gather to TaskGroup: every pytest.raises around converted code needs one of the group-aware assertions below. Going the other way — pytest.raises(ExceptionGroup) alone — passes for any failure and checks nothing useful.
Verify: search tests for pytest.raises( around code that now uses TaskGroup; each should use group_contains or RaisesGroup.
2. Check membership with group_contains¶
When the test cares that a particular error is in the group, but not about everything else, catch the group and query it:
async def test_reports_bad_row():
with pytest.raises(ExceptionGroup) as info:
await import_rows([good, bad_row_3, good])
assert info.group_contains(ValueError, match="row 3")
assert not info.group_contains(TypeError)
Tested: group_contains(ValueError, match="row 3") passed for a group that also held a KeyError, and group_contains(TypeError) was false. match is a regular expression searched in the exception's string, like pytest.raises(match=...). By default group_contains searches nested groups too; pass depth=1 to require the exception at the top level. This style is the right choice when other members are legitimately unpredictable — for example, which sibling tasks happened to fail before cancellation.
Verify: the assertion fails if the expected error is removed from the code path.
3. Assert the exact contents with RaisesGroup¶
When the test should fail on any unexpected error, use pytest.RaisesGroup, which requires the group's members to match exactly:
async def test_both_failures_reported():
with pytest.RaisesGroup(ValueError, KeyError):
await batch(ValueError("v"), KeyError("k"))
async def test_only_validation_errors():
with pytest.RaisesGroup(ValueError):
await batch(ValueError("v"), KeyError("k")) # FAILS: Unexpected exception(s): [KeyError('k')]
async def test_message_of_member():
with pytest.RaisesGroup(pytest.RaisesExc(ValueError, match="row 3")):
await batch(ValueError("bad row 3"))
Tested: the exact form passed for [ValueError, KeyError], and RaisesGroup(ValueError) failed with "1 matched exception. Unexpected exception(s): [KeyError('k')]" — a new, unplanned failure mode surfaces as a test failure instead of hiding inside a group. Order does not matter; counts do. pytest.RaisesExc adds message matching or a check= callable for each member.
Verify: introducing an extra failing task in the code under test makes the RaisesGroup test fail.
RaisesGroup also takes a check= callable for the group itself — for example to assert the group's message ("3 of 7 items failed") or a field on a custom group type — and its match= argument matches the group's own message rather than its members'. Keeping the member assertions and the group assertions separate makes failures easy to read: pytest reports which member did not match and why.
4. Handle nesting and bare exceptions explicitly¶
Groups nest when TaskGroups nest, and some code paths raise either a bare exception or a group. Tell RaisesGroup what to accept:
async def test_nested_task_groups():
with pytest.RaisesGroup(ValueError, ValueError, flatten_subgroups=True):
await outer_job() # raises ExceptionGroup([ValueError, ExceptionGroup([ValueError])])
async def test_validation_before_fanout():
with pytest.RaisesGroup(ValueError, allow_unwrapped=True):
await import_rows(rows=[]) # raises a bare ValueError before any TaskGroup starts
async def test_nested_structure_matters():
with pytest.RaisesGroup(ValueError, pytest.RaisesGroup(ValueError)):
await outer_job() # asserts the exact nesting
All three forms were tested. flatten_subgroups=True asserts the set of leaf exceptions regardless of how deeply they are nested, which is usually what a behaviour test wants; nesting a RaisesGroup asserts the structure too, which suits tests of code that builds groups on purpose, as in flattening nested ExceptionGroups. allow_unwrapped=True lets one assertion cover a function that validates input before fanning out.
Verify: each nested-group test states whether it checks structure or only leaves.
5. Make group contents deterministic where you can¶
Tests of groups can be flaky because which siblings fail depends on timing: when one task fails, the TaskGroup cancels the rest, and a sibling that would have failed later never does. Tested earlier in this section: a task failing at 0.01 s and another that would have failed at 0.5 s produced a group with only the first:
async def test_all_items_reported(fake_clock):
# Make every failure happen before the group starts cancelling: fail synchronously at start
with pytest.RaisesGroup(ValueError, ValueError):
await batch(ValueError("a"), ValueError("b")) # both fail on the first await
# When failures are timing-dependent, assert membership instead of exact contents
async def test_any_item_failure_is_reported():
with pytest.raises(ExceptionGroup) as info:
await process_with_variable_latency(items)
assert info.group_contains(ValueError)
Control time with fakes so failures happen in a known order, as in controlling time in asyncio tests, or use the lenient assertion where the set of failures is genuinely timing-dependent. A test that passes or fails depending on scheduling is worse than a looser test that always means the same thing.
Verify: the group tests pass reliably when run 100 times (pytest --count=100 with pytest-repeat).
Verification¶
Group-raising code is tested well when:
- No
pytest.raises(SomeError)wraps TaskGroup code. - Strict
RaisesGroupassertions are used where the failure set is deterministic. group_containsis used where other members vary.- Nesting and bare-exception paths are asserted explicitly.
- Group messages and custom fields are checked with
match=orcheck=where they matter.
Diagnostic Hook: when a group assertion fails, pytest prints which members matched and which were unexpected. Read those lines first: an unexpected member is usually a new failure mode in the code, while a missing one often means a sibling was cancelled before it could fail — a timing issue in the test rather than a bug.
Pitfalls & edge cases¶
pytest.raises(ValueError)around TaskGroups. Tested: it fails.pytest.raises(ExceptionGroup)alone. Passes for any failure; asserts nothing specific.- Exact assertions on timing-dependent groups. Cancelled siblings make them flaky.
- Forgetting nested groups. Use
flatten_subgroupsor nestedRaisesGroup.
Frequently Asked Questions¶
How do I test that a TaskGroup raised a ValueError in pytest?
Use pytest.RaisesGroup(ValueError) for an exact match, or pytest.raises(ExceptionGroup) as info followed by info.group_contains(ValueError). Plain pytest.raises(ValueError) fails against a group.
What is pytest.RaisesGroup?
A context manager added in pytest 8.4 that asserts an ExceptionGroup with exactly the given member types, with options for message matching, flattening nested groups and accepting a bare exception.
How do I match the message of an exception inside a group?
Use info.group_contains(Type, match="regex"), or pass pytest.RaisesExc(Type, match="regex") as a member of pytest.RaisesGroup.
Why are my ExceptionGroup tests flaky?
A failing task cancels its siblings, so which other failures appear depends on timing. Make failures happen in a controlled order or assert membership instead of exact contents.
Related¶
- Exception Groups & TaskGroups — up to the topic overview.
- Handling specific errors with except* — the production side of the same groups.
- Resilience, Cancellation & Error Handling — the section overview.