This message is longer than I had anticipated.
To aid comprehension, I'm:
- accepting that .split and .subgroup help my "handle some excpetions but not others" situation, barely
- arguing for ExceptionGroups acting like other containers: truthy if nonempty, falsey if empty; iterable; .subgroup and .split _not_ returning None for an empty subgroup, so that the container-like aspects can be used directly
On 28Feb2021 10:40, Irit Katriel firstname.lastname@example.org wrote:
split() and subgroup() take care to preserve the correct metadata on all the internal nodes, and if you just use them you only make safe operations. This is why I am hesitating to add iteration utilities to the API. Like we did, people will naturally try that first, and it's not the safest API.
Wouldn't it be safe if the ExceptionGroup were immutable, as you plan? Or have you another hazard in mind?
We actually have the OSErrors example in the PEP, just above https://www.python.org/dev/peps/pep-0654/#caught-exception-objects:
try: low_level_os_operation() except *OSerror as errors: raise errors.subgroup(lambda e: e.errno != errno.EPIPE) from None
Indeed. That basicly addresses my pattern. Along with:
On Sun, Feb 28, 2021 at 6:30 AM Guido van Rossum email@example.com wrote:
There’s a pattern for what you propose using the split() method and a lambda, or you can keep the exceptions in a list and re-wrap them at the end.
The keep-a-list approach was my fallback, absent a way to push an unhandled exception back in some sense.
We really don’t want users pushing non-exceptions into the list, nor do we want e.g. KeyboardInterrupt to be added to a (non-Base-) ExceptionGroup.
I was only imagining pushing exceptions from the original group back in. Enforcing that would be tedious and confusing though, so I was imagining some way of marking specific subexeceptions as handled or not handled.
But I had not understood the subgroup method.
I think my handled/unhandled concerns are (barely) sufficient addressed above. If I wanted to sweep the group for handled exceptions and then reraise the unhandled ones in their own ExceptionGroup at the end, that seems tractable.
But all that said, being able to iterable the subexceptions seems a natural way to manage that:
unhandled =  try: ......... except *OSError as eg: for e in eg: if an ok exception: handle it else: unhandled.append(e) if unhandled: raise ExceptionGroup("unhandled", unhandled)
There are some immediate shortcomings above. In particular, I have no way of referencing the original ExceptionGroup without surprisingly cumbersome:
try: ......... except ExceptionGroup as eg0: unhandled =  eg, os_eg = eg0.split(OSError) if os_eg: for e in os_eg: if an ok exception: handle it else: unhandled.append(e) if eg: eg, value_eg = eg.split(ValueError) if value_eg: for e in value_eg: if some_ValueError_we_understand: handle it else: unhandled.append(e) if eg: unhandled.append(eg) if unhandled: raise ExceptionGroup("unhandled", unhandled) from eg0
I have the following concerns with the pattern above:
There's no way to make a _new_ ExceptionGroup with the same __cause__ and __context__ and message as the original group: not that I can't assign to these, but I'd need to enuerate them; what if the exceptions grew new attributes I wanted to replicate?
This cries out for another factory method like .subgroup but which makes a new ExceptionGroup from an original group, containing a new sequence of exceptions but the same message and coontext. Example:
unhandled_eg = eg0.with_exceptions(unhandled)
I don't see a documented way to access the group's message.
I'm quite unhappy about .subgroup (and presumably .split) returning None when the group is empty. The requires me to have the gratuitous "if eg:" and "if value_eg:" if-statements in the example above.
If, instead, ExceptionGroups were like any other container I could just test if they were empty:
_and_ even if they were empty, iterate over them. Who cares if the loop iterates zero times? The example code would become:
try: ......... except ExceptionGroup as eg0: unhandled =  eg, os_eg = eg0.split(OSError) for e in os_eg: if an ok exception: handle it else: unhandled.append(e) eg, value_eg = eg.split(ValueError) for e in value_eg: if some_ValueError_we_understand: handle it else: unhandled.append(e) if eg: unhandled.append(eg) if unhandled: raise eg0.with_exceptions(unhandled)
You'll note that "except*" is not useful in this pattern. However...
If a subgroup had a reference to its parent this gets cleaner again:
unhandled =  eg0 = None try: ......... except* OSError as os_eg: eg0 = os_eg.__original__ # or __parent__ or something for e in os_eg: if an ok exception: handle it else: except* ValueError as value_eg: eg0 = os_eg.__original__ # or __parent__ or something for e in value_eg: if some_ValueError_we_understand: handle it else: unhandled.append(e) except* Exception as eg: eg0 = os_eg.__original__ # or __parent__ or something unhandled.extend(eg) if unhandled: raise eg0.with_exceptions(unhandled)
Except that here I have no way to get "eg0", the original ExceptionGroup, for the final raise without the additional .__original__ attribute.
Anyway, I'm strongly of the opinion that ExceptionGroups should look like containers, be iterable, be truthy/falsey based on empty/nonempty and that .split and .subgroup should return empty subgroups instead of None.
Cheers, Cameron Simpson firstname.lastname@example.org