
On 2021-09-30 4:15 a.m., Steven D'Aprano wrote:
On Thu, Sep 30, 2021 at 12:03:37AM -0300, Soni L. wrote:
Only some.user_code is guarded by the try block. If it turns out that code_we_assume_is_safe is not actually safe, and fails with an exception, it won't be caught by the try block and you will know about it.
Except no, because ExceptionWeCareAbout is part of the public API. [...]
You have not convinced me that I have misunderstood the proposal. As I said, some better, runnable code might help. But for the sake of the argument, suppose I have misunderstood and your analysis is correct..
You have just demonstrated that your proposed syntax hurts readability. In your original function, it is easy to recognise potentially poor exception hygiene at a glance:
"Too much stuff inside a try block == potential bad hygiene"
With your proposed syntactic sugar, there is no visible try block, and it is exceedingly unclear which parts of the function body are protected by an implicit try block, and which parts will have the exception caught and turned into RuntimeError, and which parts will have the exception caught and re-raised.
You misnderstand exception hygiene. It isn't about "do the least stuff in try blocks", but about "don't shadow unrelated exceptions into your public API". For example, generators don't allow you to manually raise StopIteration anymore:
next((next(iter([])) for x in [1, 2, 3])) Traceback (most recent call last): File "<stdin>", line 1, in <genexpr> StopIteration
The above exception was the direct cause of the following exception: Traceback (most recent call last): File "<stdin>", line 1, in <module> RuntimeError: generator raised StopIteration This is a (limited) form of exception hygiene. Can we generalize it? Can we do better about it? This effectively means all generators *are* wrapped in a try/except, so your point about "too much stuff inside a try block" goes directly against accepted practice and even existing python features as they're implemented.
[no comments on the rest of your points because they're all based on this core misunderstanding.]
The rest of my post is more important.
My comments asking how the compiler is supposed to know which part of the code needs to be guarded with a "re-raise the exception" flag still apply, regardless of whether I have misunderstood your API or not.
Your syntax has:
def a_potentially_recursive_function(some, args) with ExceptionWeCareAbout: some.user_code() code_we_assume_is_safe() if args.something and some_condition: raise ExceptionWeCareAbout # Line (A)
How does the compiler know that *only* ExceptionWeCareAbout originating in Line (A) should be re-raised, and any other location turned into RuntimeError?
Same way Rust decides whether to propagate or unwrap a Result: you *must* tell the compiler.
What if I factor out those last two lines and make it:
def a_potentially_recursive_function(some, args) with ExceptionWeCareAbout: some.user_code() code_we_assume_is_safe() check_condition_or_raise(args.something, some_condition)
How does the compiler decide to re-raise exceptions originating in the last line but not the first two?
In this case, it explicitly doesn't. You explicitly told it the last line doesn't raise any exceptions that contribute to your API's exception surface. You *must* use try: check_condition_or_raise(args.something, some_condition) except ExceptionWeCareAbout: raise (Verbosity can be improved if this feature gets widely used, but it's beside the point.)
What if I use a pre-prepared exception instance, or an alias, or both?
BadThing = ExceptionWeCareAbout
ERROR = BadThing("a thing happened")
def a_potentially_recursive_function(some, args) with ExceptionWeCareAbout: some.user_code() code_we_assume_is_safe() if args.something and some_condition: raise ERROR
Your proposal doesn't make it clear how the compiler decides which parts of the body should allow the exception through and which should re-raise.
This works fine because any explicit raise will always poke through the generated try/except.