On Wed, Feb 24, 2021 at 1:38 AM Irit Katriel <iritkatriel@googlemail.com> wrote:

On Wed, Feb 24, 2021 at 4:39 AM Guido van Rossum <guido@python.org> wrote:

OTOH we might reconsider deriving ExceptionGroup from BaseException -- maybe it's better to inherit from Exception? I don't think the PEP currently has a clear reason why it must derive from BaseException. I believe it has to do with the possibility that ExceptionGroup might wrap a BaseException instance (e.g. KeyboardInterrupt or SystemExit).

That was the reason, and we also said that an ExceptionGroup escaping something that isn't supposed to be raising ExceptionGroups is a bug, so if you call an API that raises ExceptionGroups it is your responsibility to handle them.

We could make ExceptionGroup be an Exception, and refuse to wrap anything which is not an Exception. So asyncio either raises KeyboardInterrupt or an ExceptionGroup of user exceptions.

Those are quite different directions.

Indeed. I don't think we require that all wrapped exceptions derive from Exception; an important special case in asyncio is CancelledError, which doesn't derive from Exception. (Ditto for trio.Cancelled.)

It is really worth carefully reading the section I mentioned in Trio's MultiError v2 issue. But its "plan" seems complex, as it would require asyncio to define `class TaskGroupError(ExceptionGroup, Exception)` -- that was fine when we considered this *only* for asyncio, but now that we've found several other use cases it really doesn't feel right.

Here's a potentially alternative plan, which is also complex, but doesn't require asyncio or other use cases to define special classes. Let's define two exceptions, BaseExceptionGroup which wraps BaseException instances, and ExceptionGroup which only wraps Exception instances. (Names to be bikeshedded.) They could share a constructor (always invoked via BaseExceptionGroup) which chooses the right class depending on whether there are any non-Exception instances being wrapped -- this would do the right thing for split() and subgroup() and re-raising unhandled exceptions.

Then 'except Exception:' would catch ExceptionGroup but not BaseExceptionGroup, so if a group wraps e.g. KeyboardError it wouldn't be caught (even if there's also e.g. a ValueError among the wrapped errors).


class BaseExceptionGroup(BaseException):
    def __new__(cls, msg, errors):
        if cls is BaseExceptionGroup and all(isinstance(e, Exception) for e in errors):
            cls = ExceptionGroup
        return BaseException.__new__(cls, msg, errors)

class ExceptionGroup(Exception, BaseExceptionGroup):

--Guido van Rossum (python.org/~guido)