Expressions to handle raising and catching exceptions, plus coalescion

Normally people would say “No such …” but you were talking about the Python None value so I assumed it was an intentional pun!

Good plan. Scripts like that have been HUGELY informative in a number of previous discussions.

1 Like

No, I should have clarified. I think handling of exceptions might use rescue as a name. It has precedent and it’s kind of cute that it’s “saving us” as a postfix syntax for exception catching.

I think the two ideas (coalescing operators and inline exception handling) are sufficiently distinct that they should probably be kept in separate proposals or in separate sections of a proposal – I could imagine either feature being accepted while the other is rejected.

My main feedback though is that I don’t think any exception handling syntax should be structured around omission of the type. And I actually would prefer – although this is probably a somewhat contentious opinion of mine – that omitting the type is not even allowed.

Language designers and maintainers are pretty keenly aware that what you allow feeds back to developers in terms of what they can/will/should write. There are some good videos from pycons past and blog posts about this out there (sorry, no time to dig up references right now). Part of the discussion for this should probably directly address “pathologically bad usage” and “bad patterns this may encourage” and why we don’t think that it will be an issue.

For example, imagine a bare rescue syntax which catches Exception (the thing I dislike). It would allow for the following comparison:

# old
try:
    x = json.load(y)
except json.JSONDecodeError:
    x = None

# new
x = json.load(y) rescue None

Would lots of programmers prefer that second case? I think so! It looks much nicer! But it’s subtly different because the types of exceptions it handles are less well constrained. (And usually those sorts of differences are latent bugs.)

So it’s super important that any exception handling syntax doesn’t produce that kind of misaligned incentive to do something less correct because it looks clearer.

Consider the following as a version with the explicit exception type:

x = json.load(y) rescue(json.JSONDecodeError) None

Does that read well enough? Is it enough of an improvement? I’m not sure. IMO these are the challenges you need to address.

3 Likes

Why couldn’t rescue just be a regular function?

def rescue(func, f_args, f_kwargs, exc_type, rescue_value):
    try:
        return func(*f_args, **f_kwargs)
    except exc_type:
        return rescue_value

Honestly though, I fall into the ‘this is not enough for most real cases’ category and prefer not having this at a base language level.

For the same reason that if-else expressions can’t: functions always eagerly evaluate their arguments. Or, wording it another way: you have to have a rescue VALUE, not a rescue EXPRESSION (and similarly the try part, which you’ve done with a callback).

I’m not sure I follow. Do you have an example where my function wouldn’t work? The idea is it’s similar to passing a function and args to multiprocessing instead of the result of the called function.

The proposed syntax helps keep the purpose of a simple statement clear by putting its main action upfront while deferring the exception handling towards the end of the statement:

x = json.load(y) rescue(JSONDecodeError) None

while a rescue function as you suggest makes the code less readable because the exception handler wrapper is seen upfront with the main action wrapped inside:

x = rescue(json.load, (y,), {}, JSONDecodeError, None)

I get that, but I figure a lot of the time it’s more complicated than just returning a value. Maybe people log something, call something else, idk.

In the extremely simple case of just giving a value back, the function version and new syntax do the same thing.

try: result = 1/x
except ZeroDivisionError: result = print("x is zero")
# or
result = 1/x rescue(ZeroDivisionError) print("x is zero")
# or
result = 1/x except ZeroDivisionError: print("x is zero")

How would you do this with your function? Admittedly, this isn’t what I’d call GOOD code, but it demonstrates the problem with no additional dependencies. For something a bit more useful, you’ll have to imagine that there’s infrastructure to make this work:

locus = rooms[region][area][room] except KeyError: await(load(region, area, room))

You don’t want an unnecessary await point if the locus can be looked up synchronously, but if anything fails, do the full fetch. At best, you would have to have a dedicated sentinel to handle these lookups, which is a pain and requires that you pick your sentinel correctly (PEP 661 might help here but also might not).

PEP 308, which introduced the if-else expression, had a number of similar concerns and considerations. Many of them apply here as well. Function forms of these sorts of constructs are clunky at best, and often flawed or subtly wrong at worst.

2 Likes

I see. It ain’t pretty but the statement could be wrapped in a lambda or something to defer execution.

Though yeah it’s getting uglier and uglier. I guess I just prefer try/except handling.

1 Like

Yeah, now it’s REALLY clunky, and that still can’t handle await expressions (or yield, equivalently). There’s a reason some things have to be done as language constructs.

1 Like

Thanks for the feedback, I think I’m convinced, no exception type omission. I’d have liked to address the issue “unwanted function/expression failure handling” as a whole, but if it makes sense to treat coalescence and inline exception handling separately I’ll do so. If it ever does come to a PEP.

Does [a rescue expression] read well enough? Is it enough of an improvement? I’m not sure. IMO these are the challenges you need to address.

Other than running a script against github to get a feeling about potential prevalence and getting an idea about how it would be received in this thread, do you (or anyone else) know what else I can do? Is there a good place where I could e.g. run a poll?

Good points. I feel that this proposal accomplishes the main goals of the proposed defer expressions with a feasible solution by drawing the line of when to actually evaluate a defer expression at the occurrence of an exception.

Additionally to what else has been said, you’d lose static analysis on func as well.

Really, when faced with “I want to do a lookup, and if it fails just proceed with a default value” and imagining that both patterns did exist in python, you’d rather type this

try:
    value = lookup()
except LookupError:
    value = "default"

than

value = lookup() rescue(LookupError) "default"

No matter what? Is it the choice of words, the way it looks, too dense, …?

Assuming that we’re okay with an eagerly-evaluated expression for the rescue value (as opposed to something that is only evaluated when the exception is raised)… just consider what the calling code would look like:

x = rescue(json.load, (y,), {}, json.JSONDecodeError, None)

I suppose that could be improved a bit. For example:

class rescue:
    def __init__(self, exc_type, value=None):
        self._exc_type, self._value = exc_type, value
    def __call__(self, func, f_args, f_kwargs):
        try:
            return func(*f_args, **f_kwargs)
        except self._exc_type as e:
            return self._value

rescue(json.JSONDecodeError)(json.load, y)

But I think that having the proposed syntax would make the underlying idea a lot more accessible - few programmers would come up with a recipe like this independently. And it still entails syntax where the actual call doesn’t actually appear as a function call.

This could also be expanded, e.g. to optionally specify a “handler” callable for the exception. But then one would presumably want to be able to design such the handler could also take advantage of rescue directly (i.e., without a wrapper lambda)… which seems difficult.

3 Likes

Or what you’re really saying is: generic deferred expressions don’t work, and an expression syntax that JUST handles exceptions would be useful.

2 Likes

Then you misunderstood the deferred expression proposal. But that is off topic here, and I don’t really have any interest in continuing the discussion with @Rosuav.

General point: defer expression would allow a pretty decent (depending on the exact syntax choice) implementation of rescue as a normal function, but this topics proposal has no other relation to mine. This is similar to how a good macro proposal would allow implementing rescue.

Yeah, I get that your proposal is far more than exception handling with a default value, but I feel that it would be the biggest use case.

Which is interesting, considering that is not even a usecase I had in mind or was mentioned in that thread (almost as if this would actually be a useful feature if we found a good syntax…)

I do like PEP 638 – Syntactic Macros | peps.python.org, and am still miffed that it’s not going anywhere.

The syntax just doesn’t look right to me. I guess that could change if I see it more and more. I guess the syntactic sugar is less sweet than the full form code in my head.

In my current head, this looks better… albeit weird:

# inline try except where it returns a value.
value = try: thing() except ValueError: 'potato'