Exception chaining and groups
When you catch one error and raise another, record why. raise New(...) from err stores the original in __cause__, and the traceback shows both, with the original as the direct cause.
class ConfigError(Exception):
pass
def port(text):
try:
return int(text)
except ValueError as err:
raise ConfigError(text) from err
try:
port("http")
except ConfigError as e:
print(e, "caused by", repr(e.__cause__))Raising inside except without from still keeps the first error, in __context__, but reports it as a second failure that happened while handling the first. raise ... from None hides it.
Several errors at once
ExceptionGroup(message, errors) carries a list of exceptions, for example every problem found in a form. Each except* clause takes the matching part of the group, and anything left unhandled is raised again.
errors = [ValueError("age"),
KeyError("name")]
try:
raise ExceptionGroup("form", errors)
except* ValueError as eg:
print("values:", eg.exceptions)
except* KeyError as eg:
print("keys:", eg.exceptions)Your task
Write load_port(config) returning int(config["port"]) and raising ConfigError("bad port") chained with from to the original KeyError or ValueError. Write validate(user) that collects KeyError("name") when "name" is missing and ValueError("age") when "age" is missing or not a non-negative int, raises them together as ExceptionGroup("invalid user", errors), and returns True when there are none.
The explanation opens here once every check passes.