On Thu, Feb 25, 2021 at 2:13 PM Guido van Rossum <guido@python.org> wrote:
So is "fail-fast if you forget to handle an ExceptionGroup" really a feature? (Do we call this out in the PEP?)
We may believe that "except Exception" is an abuse, but it is too common to dismiss out of hand. I think if some app has e.g. a main loop where they repeatedly do something that may fail in many ways (e.g. handle a web request), catch all errors and then just log the error and continue from the top, it's a better experience if it logs "ExceptionGroup: <message> [<list of subexceptions>]" than if it crashes.
Yeah, 'except Exception' happens a lot in the wild, and what to do about that has been a major sticking point in the ExceptionGroup debates all along. I wouldn't say that 'except Exception' is an abuse even -- what do you want gunicorn to do if your buggy flask app raises some random exception? Crash your entire web server, or log it and attempt to keep going? (This is almost your example, but adding in the part where gunicorn is reliable and well-respected, and that its whole job is to invoke arbitrarily flaky code written by random users.) Yury/I/others did discuss the idea of a BaseExceptionGroup/ExceptionGroup split a lot, and I think the general feeling is that it could potentially work, but feels like a complicated and awkward hack, so no-one was super excited about it. For a while we also had a compromise design where only BaseExceptionGroup was built-in, but we left it non-final specifically so asyncio could define an ExceptionsOnlyExceptionGroup. Another somewhat-related awkward part of the API is how ExceptionGroup and plain-old 'except' should interact *in general*. The intuition is that if you have 'except ValueError' and you get an 'ExceptionGroup(ValueError)', then the user's code has some kind of problem and we should probably do.... something? to let them know? One idea I had was that we should raise a RuntimeError if this happens, sort of similar to PEP 479. But I could never quite figure out how this would help (gunicorn crashing with a RuntimeError isn't obviously better than gunicorn crashing with an ExceptionGroup). == NEW IDEA THAT MAYBE SOLVES BOTH PROBLEMS == Proposal: - any time an unwinding ExceptionGroup encounters a traditional try/except, then it gets replaced with a RuntimeError whose __cause__ is set to the original ExceptionGroup and whose first traceback entry points to the offending try/except block - CUTE BIT I ONLY JUST THOUGHT OF: this substitution happens right *before* we start evaluating 'except' clauses for this try/except So for example: If an ExceptionGroup hits an 'except Exception': The ExceptionGroup is replaced by a RuntimeError. RuntimeError is an Exception, so the 'except Exception' clause catches it. And presumably logs it or something. This way your log contains both a notification that you might want to switch to except* (from the RuntimeError), *along with* the full original exception details (from the __cause__ attribute). If it was an ExceptionGroup(KeyboardInterrupt), then it still gets caught and that's not so great, but at least you get the RuntimeError to point out that something has gone wrong and tell you where? If an ExceptionGroup(ValueError) hits an 'except ValueError': it doesn't get caught, *but* a RuntimeError keeps propagating out to tell you you have a problem. And when that RuntimeError eventually hits the top of your program or ends up in your webserver logs or whatever, then the RuntimeError's traceback will point you to the 'except ValueError' that needs to be fixed. If you write 'except ExceptionGroup': this clause is a no-op that will never execute, because it's impossible to still have an ExceptionGroup when we start matching 'except' clauses. (We could additionally emit a diagnostic if we want.) If you write bare 'except:', or 'except BaseException': the clause always executes (as before), but they get the RuntimeError instead of the ExceptionGroup. If you really *wanted* the ExceptionGroup, you can retrieve it from the __cause__ attribute. (The only case I can think of where this would be useful is if you're writing code that has to straddle both old and new Python versions *and* wants to do something clever with ExceptionGroups. I think this would happen if you're implementing Trio, or implementing a higher-level backport library for catching ExceptionGroups, something like that. So this only applies to like half a dozen users total, but they are important users :-).) -n -- Nathaniel J. Smith -- https://vorpus.org