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).
Pseudo-code:
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):
pass