Python Module 7 – Errors, exceptions and context managers
Exception groups and except*
Raise several exceptions at once with ExceptionGroup, handle each kind with except*, filter groups with split() and subgroup(), and read TaskGroup errors.
What you will learn
- Build ExceptionGroup objects and inspect what they hold
- Handle the exceptions of a group with except*
- Filter nested groups with split() and subgroup()
- Explain why concurrent code can report several errors at once
- Choose between except and except* for code that may raise groups
Before you start
What you’ll learn · Before you start
What you will learn
- Build ExceptionGroup objects and inspect what they hold
- Handle the exceptions of a group with except*
- Filter nested groups with split() and subgroup()
- Explain why concurrent code can report several errors at once
- Choose between except and except* for code that may raise groups
Before you start
On this page
Sometimes one exception is not enough. A form with three bad fields should say so all at once, not one fix at a
time, and when five downloads run side by side, two of them can fail at the same moment. Since Python 3.11, an
exception group: An exception that carries several others, made with ExceptionGroup(message, exceptions). except* handles each kind of exception inside it, and a group can hold other groups. carries several exceptions together:
raise ExceptionGroup("the form has errors", errors) raises a list of them as one exception. The except* clause
takes a group apart by type, so except* ValueError: handles every ValueError inside the group and lets the rest
travel on. Groups are ordinary exceptions otherwise: they have a message, they can be nested, and their methods
split() and subgroup() filter them.
Below you will raise groups, step through except* taking one apart, filter nested groups by hand, and follow where
most groups come from: tasks that run at the same time.
An ExceptionGroup raises several exceptions as one
ExceptionGroup(message, exceptions) takes a message and a non-empty list of exception objects. Validation is the
everyday use: check every field, collect what is wrong, and raise it all together.
class FieldError(ValueError):
def __init__(self, field, problem):
super().__init__(field, problem)
self.field = field
self.problem = problem
def __str__(self):
return f"{self.field}: {self.problem}"
def validate_signup(form):
errors = []
if not form.get("name", "").strip():
errors.append(FieldError("name", "is empty"))
if "@" not in form.get("email", ""):
errors.append(FieldError("email", "needs an @"))
age = form.get("age")
if not isinstance(age, int) or not 13 <= age <= 120:
errors.append(FieldError("age", f"must be a whole number from 13 to 120, got {age!r}"))
if errors:
raise ExceptionGroup("the sign-up form has errors", errors)
return form
print(validate_signup({"name": "Kiran", "email": "kiran@example.com", "age": 19})["name"], "signed up")
validate_signup({"name": " ", "email": "kiran.example.com", "age": "19"}) Output (exit status 1)
Kiran signed up
Printed as an error (standard error)
+ Exception Group Traceback (most recent call last):
| File "form_errors.py", line 26, in <module>
| validate_signup({"name": " ", "email": "kiran.example.com", "age": "19"})
| ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| File "form_errors.py", line 21, in validate_signup
| raise ExceptionGroup("the sign-up form has errors", errors)
| ExceptionGroup: the sign-up form has errors (3 sub-exceptions)
+-+---------------- 1 ----------------
| FieldError: name: is empty
+---------------- 2 ----------------
| FieldError: email: needs an @
+---------------- 3 ----------------
| FieldError: age: must be a whole number from 13 to 120, got '19'
+------------------------------------
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 form_errors.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The traceback shows the group’s message with a count, “(3 sub-exceptions)”, then each exception it holds in a numbered box, so a single run reports everything that needs fixing. A few rules apply when you build one:
- The list must not be empty;
ExceptionGroup("x", [])raisesValueError. - It holds exception objects, not classes, and only subclasses of
Exception. A group that has to carry aKeyboardInterruptis aBaseExceptionGroup, whichexcept Exception:does not catch. - The group keeps its message in
.messageand its exceptions, as a tuple, in.exceptions. A group can hold other groups, which is how a form with an address section can report the address’s own errors as one entry.
FieldError derives from ValueError, so each entry is still recognisable as a bad value, and it keeps the field’s
name as an attribute for whoever reports the errors.
A group for an error that is always alone
A group earns its place when there can be several independent errors, as in a form or a set of tasks. A function
that can only ever fail in one way is clearer raising that one ValueError itself.
Checkpoint
Check: Raising several exceptions as one group
except* handles each kind in a group once
except* clauses take a group apart by type. This program checks orders that can be wrong in several ways at once:
def check(order):
problems = []
if not isinstance(order.get("qty"), int):
problems.append(TypeError(f"qty must be an int, got {type(order.get('qty')).__name__}"))
elif order["qty"] < 1:
problems.append(ValueError(f"qty must be at least 1, got {order['qty']}"))
if order.get("coupon") not in (None, "WELCOME10"):
problems.append(ValueError(f"unknown coupon {order['coupon']!r}"))
if "address" not in order:
problems.append(KeyError("address"))
if problems:
raise ExceptionGroup(f"order {order['id']} is invalid", problems)
orders = [
{"id": 1, "qty": 0, "coupon": "FREE", "address": "Pune"},
{"id": 2, "qty": "2", "address": "Kochi"},
{"id": 3, "qty": 1},
]
for order in orders:
try:
check(order)
except* ValueError as group:
print(f"order {order['id']}, bad values:", "; ".join(str(e) for e in group.exceptions))
except* TypeError as group:
print(f"order {order['id']}, wrong type:", group.exceptions[0]) Output (exit status 1)
order 1, bad values: qty must be at least 1, got 0; unknown coupon 'FREE' order 2, wrong type: qty must be an int, got str
Printed as an error (standard error)
+ Exception Group Traceback (most recent call last):
| File "except_star.py", line 22, in <module>
| check(order)
| ~~~~~^^^^^^^
| File "except_star.py", line 12, in check
| raise ExceptionGroup(f"order {order['id']} is invalid", problems)
| ExceptionGroup: order 3 is invalid (1 sub-exception)
+-+---------------- 1 ----------------
| KeyError: 'address'
+------------------------------------
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 except_star.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Order 1 has two bad values, and the except* ValueError clause ran once, with a group of both of them in
group. Order 2 had a wrong type, which the second clause handled. Order 3 lacked its address: no clause names
KeyError, so after the clauses had run, Python raised what was left, a group of one, and the program ended there.
That is the difference from except: several clauses can run for one group, each for its own part, and nothing is
silently dropped. A group with all four kinds of problem at once goes through the clauses like this:
How except* clauses split an exception group
Text description of the diagram
The diagram is one column of four boxes, read from top to bottom.
- At the top is the exception group that was raised. It holds two ValueErrors, a TypeError and a KeyError.
- The first clause, except* ValueError, splits the group: it takes the two ValueErrors and runs once, with a group that holds just those two.
- The rest of the group, the TypeError and the KeyError, goes on to the next clause, except* TypeError, which takes the TypeError and runs once with it.
- The KeyError matched no clause. After the clauses have run, Python raises it again, inside a group of one with the original group's message, so the caller can still see it.
Step through that group, built from the problems of the three orders, and watch which clauses run and what group
holds in each. An outer try statement catches whatever the clauses leave, so you can see it:
Program · star_trace.py
errors = [ValueError("qty is 0")]
errors.append(ValueError("unknown coupon"))
errors.append(TypeError("qty is a str"))
errors.append(KeyError("address"))
try:
try:
raise ExceptionGroup("order 4", errors)
except* ValueError as group:
print("values:", len(group.exceptions))
except* TypeError as group:
print("types:", len(group.exceptions))
except ExceptionGroup as rest:
print("left over:", rest.exceptions)
print("done") Variables
- changed in this step
Printed so far
Nothing printed yetvalues: 2 types: 1 left over: (KeyError('address'),) done
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 15 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
errors = [ValueError("qty is 0")]makeserrorsa list of 1 item. - Line 2.
errors.append(ValueError("unknown coupon"))addsValueError('unknown coupon')to the end oferrors. - Line 3.
errors.append(TypeError("qty is a str"))addsTypeError('qty is a str')to the end oferrors. - Line 4.
errors.append(KeyError("address"))addsKeyError('address')to the end oferrors. - Line 5.
try:runs. - Line 6.
try:runs. - Line 7.
raise ExceptionGroup("order 4", errors)runs. The group holds four exceptions, two of themValueError, oneTypeErrorand oneKeyError: the group of the diagram above. - Line 8. Line 8 runs. Key point
except* ValueErrorsplits the group. Both of itsValueErrorexceptions go intogroup, a newExceptionGroupwith the message "order 4", and the clause runs once for the two. - Line 9.
print("values:", len(group.exceptions))printsvalues: 2.groupno longer exists. - Line 10. Line 10 runs. The rest, the
TypeErrorand theKeyError, reaches the next clause, which takes theTypeError. - Line 11.
print("types:", len(group.exceptions))printstypes: 1.groupno longer exists. - Line 12. Line 12 runs. Key point Both clauses ran, once each. The
KeyErrormatched neither, so Python raised it again in a group of one with the message "order 4", and the plainexceptof the outer statement catches that group whole. - Line 13.
print("left over:", rest.exceptions)printsleft over: (KeyError('address'),).restno longer exists. - Line 14.
print("done")printsdone. That was the last line, so the program ends.
Key idea
Each except* clause runs at most once, with a group of every exception it matches; the rest goes on to the next
clause, and whatever no clause matches is raised again.
Checkpoint
Check: How except* takes a group apart
except* follows stricter rules than except
A clause that may share a group with other clauses has to keep to a few rules that plain except does not have. This
program shows the first two:
def risky(kind):
if kind == "single":
raise ValueError("a single error")
raise ExceptionGroup("two errors", [TypeError("bad type"), OSError("disk full")])
try:
risky("single")
except* ValueError as group: # a plain exception arrives wrapped in a group
print("wrapped:", repr(group))
try:
risky("group")
except* TypeError, OSError: # Python 3.14: no brackets needed without "as"
print("one clause, run once, for both errors") Output
wrapped: ExceptionGroup('', (ValueError('a single error'),))
one clause, run once, for both errors
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 star_rules.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
- An exception that is not a group still reaches an
except*clause, wrapped in a group with an empty message, as the first line shows. Inside anexcept*clause the name always refers to a group. - A clause with several types runs once for all of them together. Since Python 3.14 the brackets around the types can
go, as with
except, as long as there is noas. - One
trystatement uses eitherexceptorexcept*, never both: mixing them is aSyntaxError. except* ExceptionGroup:is refused with aTypeError, because it would be unclear which part it means; catch a whole group with plainexcept ExceptionGroup:instead.break,continueandreturncannot appear in anexcept*clause, because other clauses may still have to run.
Checkpoint
Check: The rules of except*
split() and subgroup() filter a group and keep its nesting
except* is built on two methods that you can call yourself, for example when one function collects errors and
another one reports them. group.subgroup(condition) keeps the exceptions that match, and group.split(condition)
returns the match and the rest as a pair. The condition is an exception type, a tuple of types, or a function that
takes an exception and returns true for the ones to keep:
report = ExceptionGroup(
"profile has errors",
[
ValueError("name is empty"),
TypeError("age must be an int"),
ExceptionGroup("address has errors", [ValueError("PIN code needs 6 digits"), KeyError("city")]),
],
)
values, rest = report.split(ValueError)
print("match:", repr(values))
print("rest: ", repr(rest))
print("keys: ", repr(report.subgroup(lambda e: isinstance(e, KeyError))))
print("none: ", report.subgroup(OSError))
def leaves(group):
"""Every exception inside a group, however deeply nested, in order."""
found = []
for exc in group.exceptions:
if isinstance(exc, BaseExceptionGroup):
found.extend(leaves(exc))
else:
found.append(exc)
return found
print([f"{type(e).__name__}: {e}" for e in leaves(report)]) Output
match: ExceptionGroup('profile has errors', [ValueError('name is empty'), ExceptionGroup('address has errors', [ValueError('PIN code needs 6 digits')])])
rest: ExceptionGroup('profile has errors', [TypeError('age must be an int'), ExceptionGroup('address has errors', [KeyError('city')])])
keys: ExceptionGroup('profile has errors', [ExceptionGroup('address has errors', [KeyError('city')])])
none: None
['ValueError: name is empty', 'TypeError: age must be an int', 'ValueError: PIN code needs 6 digits', "KeyError: 'city'"]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 split_subgroup.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Step through the calls of the program on its nested report:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The report. split_subgroup.py builds one group, "profile has errors", that holds a ValueError, a TypeError and a nested group for the address, which holds a ValueError and a KeyError of its own.
- split(ValueError) finds both. report.split(ValueError) picks every ValueError, however deep it sits. The empty name is at the top level, and the PIN code is inside the address group.
- The match keeps the nesting. The match is a group "profile has errors" whose entries are the name's ValueError and a group "address has errors" holding the PIN error, the first line of the output.
- The rest keeps it too. Everything else is the rest, with the same shape. The TypeError of the age sits at the top, and the KeyError of the city inside its own address group.
- subgroup() with a test. report.subgroup(lambda e: isinstance(e, KeyError)) keeps only the city's KeyError, still inside both groups. Entries that hold no match are left out.
- Nothing to keep. report.subgroup(OSError) matches nothing at all, so it returns None rather than an empty group, the fourth line of the output.
Both methods keep the nesting: the ValueError of the address stays inside an “address has errors” group in the
match, and the KeyError stays inside one in the rest. Groups that would be empty are left out, and a result with
nothing in it is None, as subgroup(OSError) shows. When you need the exceptions themselves, however deep they
sit, walk the groups with a small recursive function such as leaves(). Notes, the cause and the traceback of the
original group are copied to the groups that split() and subgroup() return.
Hiding the rest
A handler that catches ExceptionGroup and looks only for the errors it knows quietly drops the others. With
except*, or with split() and raise rest, the unknown ones go on to the caller.
Checkpoint
Check: Filtering with split() and subgroup()
Tasks that fail together report a group
Concurrent code was the main reason exception groups were added. When several tasks run at the same time and two of them fail, there is no honest way to raise only one of the two errors. asyncio.TaskGroup runs tasks together inside an async with block. When one of them fails, it cancels the others still running, and once all have finished it raises their errors as one ExceptionGroup., the subject of the asyncio module of this track, runs tasks together and reports their failures as a group:
import asyncio
async def fetch(source, seconds, error=None):
await asyncio.sleep(seconds)
if error:
raise error
print(f"{source}: done")
async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(fetch("prices", 0))
tg.create_task(fetch("stock", 0, ConnectionError("the stock service refused the connection")))
tg.create_task(fetch("reviews", 0, TimeoutError("reviews took too long")))
tg.create_task(fetch("images", 10)) # still running when the others fail, so it is cancelled
try:
asyncio.run(main())
except* ConnectionError as group:
print(f"{group.message}: retry later:", [str(e) for e in group.exceptions])
except* TimeoutError as group:
print(f"{group.message}: timed out:", [str(e) for e in group.exceptions]) Output
prices: done unhandled errors in a TaskGroup: retry later: ['the stock service refused the connection'] unhandled errors in a TaskGroup: timed out: ['reviews took too long']
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 taskgroup_errors.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The prices task finished. The stock and reviews tasks both failed, and the task group collected both errors into an
ExceptionGroup with the message “unhandled errors in a TaskGroup”. When the first error arrived, the group
cancelled the images task, which was still waiting, so it neither finished nor added an error of its own. Each
except* clause then dealt with one kind of failure: retry later, or report a timeout. Step through the run:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Four tasks start. main() creates four tasks in one asyncio.TaskGroup. prices, stock and reviews wait 0 seconds; images waits 10.
- prices finishes. prices wakes up and prints "prices: done", the first line of the output.
- Two tasks fail. stock raises ConnectionError and reviews raises TimeoutError. Neither task handles its error, so both reach the TaskGroup.
- The rest is cancelled. The first failure makes the group cancel every task still running. images was still waiting, so it is cancelled and adds no error of its own.
- One group for both errors. Once all four tasks have finished, the TaskGroup combines the two errors in an ExceptionGroup with the message "unhandled errors in a TaskGroup", and raises it.
- Retry later. except* ConnectionError takes the stock error out of the group and prints that the stock service should be tried again later.
- Timed out. except* TimeoutError takes what is left, the reviews error. Both clauses have run once, nothing remains, and the program ends normally.
Reading meaning into the order of the errors
In concurrent code the order of the errors in a group depends on which task failed first. Look errors up by type,
with except* or split(), or by an attribute, never by their position in .exceptions.
Checkpoint
Check: Groups from tasks that fail together
A plain except never looks inside a group
A group is an exception like any other, so except ExceptionGroup: and even except Exception: catch it whole. What
does not catch it is a clause for one of the types inside:
def validate():
raise ExceptionGroup("2 errors", [ValueError("bad name"), ValueError("bad age")])
try:
try:
validate()
except ValueError:
print("Not printed: a group is not a ValueError.")
except ExceptionGroup as group:
print("The group went past except ValueError:", group)
print("except Exception would catch it too:", isinstance(group, Exception)) Output
The group went past except ValueError: 2 errors (2 sub-exceptions) except Exception would catch it too: True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 group_vs_except.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Catching a group with except SomeError
except ValueError: around code that raises groups never runs, so the errors it was written for go straight past
it. Write except* ValueError: instead.
except ValueError: compares the type of the exception it receives, and an ExceptionGroup is not a ValueError,
whatever it contains. So when code that you call may raise groups, as a TaskGroup does, handle its failures with
except*. Use plain except when you deal with the group as one thing, for example to log it before it goes on.
Note
Code that already catches single exceptions keeps working until something starts raising groups. A library that switches from raising one exception to raising a group changes what its callers must write: PEP 654 calls raising a group an API-breaking change, one that belongs in the library’s release notes.
Checkpoint
Check: except or except*
Practise: report every bad field, then summarise them
The exercise asks for validate_user(data), which checks three fields and raises one group with a FieldError for
every field that is wrong, and summarise_errors(group), which turns such a group into a dictionary of problems,
however deeply it is nested, and raises again whatever is not a field error. Its sample tests run in your browser:
Exercise · Medium · Python
Report every bad field, then summarise them
users.py defines FieldError(field, problem), an exception for one bad field of a form, with .field and .problem. Write two functions.
validate_user(data) checks a dictionary and returns it when every field is valid. Otherwise it raises one ExceptionGroup with the message invalid user, holding a FieldError for every bad field, in this order:
namemust be text that is not empty after.strip(); otherwiseFieldError("name", "is required").emailmust be text with exactly one@, and something before and after it; otherwiseFieldError("email", "is not an email address").agemust be anint(not abool) from 13 to 120; otherwiseFieldError("age", "must be a whole number from 13 to 120").
A missing key counts as a bad field. Even a single bad field is reported as a group of one.
summarise_errors(group) turns such a group into a dictionary from each field to its problem, for every FieldError in it, however deeply groups are nested inside it: a sign-up form might add the group of an address sub-form. Use group.split(FieldError) to separate the field errors from everything else. When the group also holds other exceptions, such as a TypeError from a bug, raise that rest of the group again instead of hiding it.
Starter code · users.py
class FieldError(Exception):
"""A problem with one field of a form."""
def __init__(self, field, problem):
super().__init__(field, problem)
self.field = field
self.problem = problem
def __str__(self):
return f"{self.field}: {self.problem}"
def validate_user(data):
"""Return data when every field is valid; otherwise raise an ExceptionGroup of FieldError, one per bad field."""
# Replace this line with your code.
return data
def summarise_errors(group):
"""Return {field: problem} for every FieldError in group, at any depth; raise the other exceptions again."""
# Replace this line with your code.
return {} The sample tests · test_users.py
from users import FieldError, summarise_errors, validate_user
GOOD = {"name": "Kiran", "email": "kiran@example.com", "age": 19}
def group_from(data):
"""The ExceptionGroup that validate_user(data) raises, or None."""
try:
validate_user(data)
except ExceptionGroup as group:
return group
return None
def test_valid_user():
"""returns the data when every field is valid"""
assert validate_user(dict(GOOD)) == GOOD
assert validate_user({"name": " Ana ", "email": "a@b.in", "age": 13})["age"] == 13
def test_every_bad_field():
"""reports all three bad fields at once, in order"""
group = group_from({"name": " ", "email": "kiran.example.com", "age": 7})
assert group is not None
assert group.message == "invalid user"
assert [type(e) for e in group.exceptions] == [FieldError, FieldError, FieldError]
assert [e.field for e in group.exceptions] == ["name", "email", "age"]
def test_one_bad_field_is_still_a_group():
"""raises a group of one for a single bad field"""
group = group_from({"name": "Kiran", "email": "kiran@@example.com", "age": 19})
assert [(e.field, e.problem) for e in group.exceptions] == [("email", "is not an email address")]
def test_missing_and_odd_values():
"""treats missing keys, a bool age and an @ at the edge as bad fields"""
assert [e.field for e in group_from({}).exceptions] == ["name", "email", "age"]
assert [e.field for e in group_from({**GOOD, "age": True}).exceptions] == ["age"]
assert [e.field for e in group_from({**GOOD, "email": "kiran@"}).exceptions] == ["email"]
assert [e.field for e in group_from({**GOOD, "age": 121}).exceptions] == ["age"]
def test_summarise_flat_group():
"""maps each field to its problem"""
group = group_from({"name": "", "email": "kiran@example.com", "age": "19"})
assert summarise_errors(group) == {"name": "is required", "age": "must be a whole number from 13 to 120"}
def test_summarise_nested_groups():
"""finds field errors at any depth"""
address = ExceptionGroup("invalid address", [FieldError("pin", "needs 6 digits"), FieldError("city", "is required")])
group = ExceptionGroup("invalid sign-up", [FieldError("name", "is required"), ExceptionGroup("sub-forms", [address])])
assert summarise_errors(group) == {"name": "is required", "pin": "needs 6 digits", "city": "is required"}
def test_summarise_raises_the_rest():
"""raises the exceptions that are not field errors again, as a group"""
group = ExceptionGroup("mixed", [FieldError("name", "is required"), TypeError("a bug")])
try:
summarise_errors(group)
except ExceptionGroup as rest:
assert [type(e) for e in rest.exceptions] == [TypeError]
return
assert False, "summarise_errors should raise the TypeError again" A hint
In validate_user, collect the errors in a list, one if per field, and raise ExceptionGroup("invalid user", errors) at the end only when the list is not empty. type(age) is int is false for True, which isinstance(True, int) would accept. In summarise_errors, start with fields, rest = group.split(FieldError); rest is None when there is nothing else, and fields keeps the nesting, so walk it with a small recursive function that collects the exceptions inside every nested group.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
To check yourself on groups, except*, splitting and task groups, answer the questions below. They follow your
answers, weakest idea first, with a reason under every choice:
Practice that adapts to you
Practise this lesson
19 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
2–5 years · What does except* ValueError do with an ExceptionGroup that holds a ValueError and a
TypeError?
It splits the group by type. The clause runs once, and its name refers to a new ExceptionGroup with the original
message that holds only the ValueError, keeping any nesting. The TypeError part is passed on to the next except*
clause of the same try statement. If no clause matches it, Python raises it again after the clauses have run, as a
group with the original message, so it reaches the caller. Each clause runs at most once per group, however many
matching exceptions the group holds. A plain except ValueError: would not match at all, because the group itself is
not a ValueError.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A request handler fetches prices, stock and reviews in an
asyncio.TaskGroup. Itsexcept TimeoutError:never runs: when the reviews service times out, the request fails with anExceptionGroupand the prices are lost. Why, and how do you return partial results while unexpected errors still surface? -
Mid to senior
Design how a sign-up API reports every invalid field at once, including those of a nested address form, as a JSON list for the client, while a programming error such as a
TypeErrorstill produces a 500. Which exceptions do you define, and where do you catch what?
Key takeaways
ExceptionGroup(message, [exc, …])raises several exceptions as one; it has.messageand.exceptions, can be nested, and must hold at least one exception.except* SomeError:runs once with a group of the matching exceptions; severalexcept*clauses can run for one group, and whatever none of them matches is raised again.trystatements useexceptorexcept*, never both; anexcept*clause cannotreturn,breakorcontinue, and cannot nameExceptionGroup. Since Python 3.14,except* A, B:needs no brackets.split()andsubgroup()filter a group by type or with a function, keep its nesting and its notes, and returnNonefor an empty result.asyncio.TaskGroupreports the failures of its tasks as a group and cancels the tasks still running; handle them withexcept*, becauseexcept ValueError:never matches a group.
Terms in this lesson
- exception group
- An exception that carries several others, made with ExceptionGroup(message, exceptions). except* handles each kind of exception inside it, and a group can hold other groups.
References
- The Python Tutorial: raising and handling multiple unrelated exceptions (Python Software Foundation)
- Built-in Exceptions: exception groups (Python Software Foundation)
- The Python Language Reference: except* clause (Python Software Foundation)
- PEP 654: Exception Groups and except* (Python Software Foundation)
- What's New In Python 3.11: exception groups and except* (Python Software Foundation)
- What's New In Python 3.14: except and except* without brackets (PEP 758) (Python Software Foundation)
- asyncio: task groups (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress