Hello, On Sat, 27 Mar 2021 10:55:40 +0000 Irit Katriel <iritkatriel@googlemail.com> wrote:
One of the motivations for introducing ExceptionGroup as a builtin is so that we won't have a different custom version in each library that needs it. So if you are writing a library the needs to raise multiple exceptions, and then you decide to call Trio, you don't need to translate Trio's MultiError into your own exception group type, because everybody uses the builtin.
Looking from a different angle shows a different perspective: 1. Trio devised an interesting concept of "nurseries" to deal with multiple tasks in asyncio-like programming. 2. It was all nice and beautiful until ... it came to error handling. Note that it's a rather typical situation - one can write nice, clean, beautiful code, which is not adequate in real-world scenarios, particularly because of error handling concerns. Oftentimes, such initial code/concepts are discarded, and more robust (though maybe not as beautiful) concepts/code is used. 3. But that's not what happened in the Trio case. The concept of nurseries got pushed forward, until it became clear that it requires changes on the programming language level. 4. That's how PEP654 was born, which, besides async scheduling of multiple tasks, bring *somewhat similar* usecases of e.g. raising exceptions thru context managers' own exceptions. Note that where errors and exceptions lead us is in questions on how to handle them. And beyond a couple of well known patterns ("dump and crash" or "dump and continue with next iteration"), error handling is very adhoc to a particular application and particular error(s). Seen from that angle, Trio wants to vendor-lock the entire language into its particular (experimental) way of handling multiple errors. So yes, maybe being well aware that you're handling exactly Trio's (or asyncio's, or context manager's) error isn't bad thing actually. And if it's clear that multiple asyncio frameworks are interested in sharing common exception base class for such usecases, then it could be introduced on the "asyncio" package level, or maybe even better, as a module similar to "concurrent.futures".
And your users don't need to learn how your particular exception group works because they know that you are using the builtin one.
I see the aesthetic value of your suggestion, but does it have practical advantages in light of the above?
The concern is that it codifies pretty complex and special-purpose things on the language level. And it seems that the whole concept is rather experimental and "original design". We can compare that with another sufficiently complex feature which landed recently: pattern matching. At all phases of the design and discussion of that feature, one of the guiding principles was: "Many other languages implement it, so we know it's generally useful, and have design space more or less charted, and can vary things to find local optimum for Python". Contrary to that, the original PEP654 didn't refer to (cross-language, cross-framework) prior art in handling multiple errors, and I don't see that changed in the latest version. So I'm sorry, but it seems like NIH feature of a specific 3rd-party framework being promoted to a whole language's way of doing things. Under such circumstance, I guess it would be good idea to try to decouple behavior of that feature from the languages core, and make aspects of behavior more explicit (following "explicit is better than implicit" principle), and allow to vary/evolve it without changing the core language. I tried to draft a scheme aspiring to allow that. (Which would definitely need more work to achieve parity with functionality in PEP654, because again, it tries to codify rather complex and "magic" behavior. Where complex and magic behavior in exception handling is itself of concern, so making it more explicit may be a good idea. (A good "how other languages deal with it") review would mention that Go, Rust, Zig, etc. don't have and condemn exception handling at all (normal simple classy exceptions, not magic we discuss here!)). Thanks, Paul
Irit
On Sat, Mar 27, 2021 at 10:31 AM Paul Sokolovsky <pmiscml@gmail.com> wrote:
Hello,
On Fri, 26 Mar 2021 16:19:26 -0700 Guido van Rossum <guido@python.org> wrote:
Everyone,
Given the resounding silence I'm inclined to submit this to the Steering Council. While I'm technically a co-author, Irit has done almost all the work, and she's done a great job. If there are no further issues I'll send this SC-wards on Monday.
One issue with PEP654 is that it introduces pretty adhoc and complex-semantics concept (ExceptionGroup) on the language level. Here's an idea (maybe duplicate) on how to introduce a much simpler, and more generic concept on the language level, and let particular frameworks to introduce (and elaborate without further changing the language) adhoc concept they need.
So, let's look how the usual "except MyExc as e" works: it performs "isinstance(e0, MyExc)" operation, where e0 is incoming exception (roughly, sys.exc_info[1]), and if it returns True, then assigns e0 to the "e" variable and executes handler body. "isinstance(e0, MyExc)" is formally known as an "exception filter".
As we see, currently Python hardcodes isinstance() as exception filter. The idea is to allow to use an explicit exception filter. Let's reuse the same "except *" syntax to specify it. Also, it's more flexible instead of returning True/False from filter, to return either None (filter didn't match), or an exception object to make available to handler (which in general may be different than passed to the filter). With this, ExceptionGroup usecases should be covered.
Examples:
1. Current implicit exception filter is equivalent to:
def implicit(e0, excs): # one or tuple, as usual if isinstance(e0, excs): return e0 return None
try: 1/0 except *implicit(ZeroDivisionError) as e: print(e)
2. Allow to catch main or chained exception (context manager example from PEP)
def chained(e, excs): while e: if isinstance(e, excs): return e e = e.__cause__ # Simplified, should consider __context__ too
try: tempfile.TemporaryDirectory(...) except *chained(OSError) as e: print(e)
3. Rough example of ExceptionGroup functionality (now not a language builtin, just implemented by framework(s) which need it, or as a separate module):
class ExceptionGroup:
...
@staticmethod def match(e0, excs): cur, rest = e0.split_by_types(excs) # That's how we allow an exception handler to re-raise either an # original group in full or just "unhandled" exception in the # group (or anything) - everything should be passed via # exception attributes (or computed by methods). cur.org = e0 cur.rest = rest return cur
try: ... except *ExceptionGroup.match((TypeError, ValueError)) as e: # try to handle a subgroup with (TypeError, ValueError) here ... # now reraise a subgroup with unhandled exceptions from the # original group raise e.rest
-- Best regards, Paul mailto:pmiscml@gmail.com