Idea of new keywords for handling exceptions (skip and elskip)

Motivation

When I have a sequence of things that I want to try in order to achieve a certain goal, I will often have to resort to the following code structure:

try:
    # Some Code that might throw MyException1 .... 
except MyException1:
    try:
        # Some Code that might throw MyException2 .... 
    except MyException2:
        try:
            # Some Code that might throw MyException3 .... 
        except MyException3:
            # Some Code that handles the case when nothing worked

While this works fine, it is IMHO quite cumbersome and unpleasant to work with.

Proposal

For this reason, and keeping in mind the PEP 20 rule “Flat is better than nested.” I would like to propose the addition of two new syntax keywords skip and elskip, such that the code above could be written in the following way:


skip MyException1:
    # Some Code that might throw MyException1 ....
elskip MyException2:
    # Some Code that might throw MyException2 ....
elskip MyException3:
    # Some Code that might throw MyException3 ....
else:
    # Some Code that handles the case when nothing worked
finally:
    # Some Code that always runs at the end

In other words, if the code inside the body of skip MyException1: raises MyException1, then it will skip that body of code and go for the next keyword (elskip/else/finally). If a skip body ends successfully (or another Exception is raised), then the whole skip block is ended (and the exception is re-raised).

The code in the else body is called if all other skip bodies raised the predicted Exceptions.

The finally keyword follows the same behavior as in the try statement (i.e. intended to define clean-up actions that must be executed under all circumstances)

Other Examples

This skip clause would also allow for a clean way to “try” a few updates:

def my_func(self):
    skip ConnectionError: self.update_nice_to_have_parameter_1()
    skip ConnectionError: self.update_nice_to_have_parameter_2()
    
    # Rest of the function ....

Just use contextlib.suppress.

from contextlib import suppress 

with suppress(MyException1):
    ...

“Simple is better than complex”. It seems to me that there are far simpler ways of refactoring those kind of cumbersome code patterns than going all out and changing the language itself.
(I also think that “elskip” conflicts with “Beautiful is better than ugly.” :slight_smile: )

How would you use suppress for their example?

skip is a confusing keyword to use for this. And while it might be nice to have less nesting, it makes the control flow less obvious. What happens if the first code block raises MyException3?

This looks a lot like a try ... except with multiple except blocks, except it’s less well-defined and it’s harder to read. I agree with @hansgeunsmeyer that the best solution is to refactor whatever has led to the example code :slightly_smiling_face:

It looks like you’re using try/except blocks to control the flow of your code. Instead of relying on try/except for control, it’s usually better to use explicit control flow tools like if statements, loops, and function calls. These methods are simpler to understand, less likely to lead to errors, and generally considered best practice.

Supress is very nice, and can replicate my last example well, but it will not work in this case because I only want to run the second code if the first raises the expected error

I do not agree that there are good refactoring options. Care to give an example?

It’s impossible to answer this without a real example of code that does this.

1 Like

Hi James,
It is not a try ... except with multiple except blocks. It is a different situation.

The first code that I provided in the Motivation I think exemplifies this well. You can imagine a situation that you have 3 ways of doing something and you only want to go to the next one if something goes wrong in the previous one

Exception 3 is only expected to be raised by codeblock 3, so if it is raised by codeblock 1, then the program with halt due to that exception. If you expect it to be raised also by codeblock 1 and want to skip to the next code as well, then you would need to do:

skip (MyException1,MyException3):
    # Some Code that might throw MyException1 (and now also MyException3) ....
...

One option would be to return from a function as soon as you’re done.

try:
    attempt_one()
    return
except FailureOptionOne:
    pass
try:
    attempt_two()
    return
except FailureOptionTwo:
    pass
...

Continue to as many levels as required.
except FailureOptionTwo:

3 Likes

Hi Elis,
I think in python it is quite typical to try something and then catch the exception.

For example, if I have 2 ways of doing something (func1 and func2) , and I prefer to do it with func1 if possible, then fallback to func2 and then if both fail, use a DEFAULT_VALUE. I would do this like:

try:
    res = func1()
except MyErr1:
    try:
        res = func2()
    except MyErr2:
        res = DEFAULT_VALUE

Hi Chris,
this definitely works.

My personal opinion is that what I am proposing would make the code much easier to read than the alternatives. But of course, there are a few ways to achieve this behavior and you suggestion definitely works.

That could be done like this:

res = DEFAULT_VALUE
for func, err in (
    (func1, MyErr1)
    (func2, MyErr2)
):
    try:
        res = func()
        break
    except err:
        pass

Or like this, after writing the tool function:

res = try_except(
    (func1, MyErr1),
    (func2, MyErr2),
    DEFAULT_VALUE
)
2 Likes

Hi Stefan,
that is also a very clever way of achieving the desired result, but maybe you would agree that it is not the most readable. Also, it would mean having a dedicated function for each code block, which often is not the case. For example, for case one you could have a function to get the raw data and another to parse it. It would be better to not create an additional function just to call these two functions.

But thanks for the interesting solution!

I understand that it’s different, I was saying that it is visually similar and the semantics are less intuitive.

It’s surprising to have multiple blocks on the same level that depend on each other but appear to be separated by conditions–i.e. the execution of elskip 3 follows the execution of elskip 2 rather than being an alternative to it. I don’t think there is any comparable syntax in the language right now, can you think of any?

One refactoring option is the one that @pochmann showed. This has the advantages of being easy to read, keeping everything related to the actual business logic centralized in one function. It’s a pretty elegant solution that can also be easily adapted/changed later.
If this is a frequent pattern in the code, then that solution could also be converted into a custom context manager, I think.
But an even simpler solution is the one suggested by @Rosuav…

Yeah. The trouble is, your proposal might be an improvement in a very small number of situations, but that’s not enough to justify new syntax. I have seen proposals with significantly better utility than this still fail to clear the bar - it’s really REALLY hard to invent new keywords and increase the complexity of every Python program out there.

Replying to both Hans and James regarding the skip and elskip notation.

Fully agree that it might not be perfect. I thought about it for a while and I didn’t find a better short word to describe the functionality. I though that perhaps it would be more correct something like try next if (Exception) but I think it could be more confusing than just skip. I also thought that attempt would be a nice candidate, but the meaning is a synonym of try, so I preferred skip

elskip is to borrow and be consistent with elif (which BTW I also don’t love :slight_smile: ). Alternative suggestions are welcome :slight_smile:

A for-else is somewhat like this. You go into the for block, then if you never break out of it, you go into the attached else block. But you’re right, this is pretty uncommon.

1 Like