Why aren't exceptions re-raised if `finally` contains a `return/break/continue`?

From the docs:

If the finally clause executes a break, continue or return statement, exceptions are not re-raised.

Why is this? I would have thought that an exception would take priority over a return/break/continue statement, especially since PEP8 explicitly discourages the use of control flow statements inside finally (see here):

Use of the flow control statements return /break /continue within the finally suite of a try...finally , where the flow control statement would jump outside the finally suite, is discouraged. This is because such statements will implicitly cancel any active exception that is propagating through the finally suite:

By m via Discussions on Python.org at 11Aug2022 23:11:

From the docs:

If the finally clause executes a break, continue or return statement, exceptions are not re-raised.

Why is this? I would have thought that an exception would take
priority over a return/break/continue statement, especially since
PEP8 explicitly discourages the use of control flow statements inside
finally (see
here):

This is so that there is a mechanism to handle exceptions (meaning to
not reraise them) from the logic in a finally statement.

If you don’t use a control flow statement, exceptions are reraised
exactly as you desire, and as most of us usually desire. But usually
is not always, and sometimes you may write a special piece of code which
wants to prevent an exception bubbling out, perhaps in a special
circumstance.

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like

It’s common to have code like this:

for thing in collection:
    try:
        value = int(thing["value"])
    except (KeyError, ValueError):
        continue
    process(value)

where, upon finding an exception, the correct thing to do is to skip this thing and move on to the next. The exception won’t be reraised; it has been completely handled.

More generally, Python does a great job of doing what you tell it. If you say “try: … except:”, Python understands that the exception has been caught; if you say “raise”, Python (re)raises an exception. You almost never need to tell Python “now do nothing”. For instance, the construct except OSError: pass is just an empty block of code in an exception handler; the “pass” doesn’t tell Python to ignore the exception.

Exceptions get handled in the place most appropriate for them, and unless the first handler reraises (indicating “nope, don’t give me this one, let someone else handle it”), the exception is considered done, finished. You can then break, continue, return, print, or anything else you want to, and Python will do that for you.

1 Like

That’s an interesting use of except

I’ve been thinking about how to catch two ‘Errors’ with one except line and it had not occurred to group them like that (would that be classed as a Tuple?) in one except clause; nice.

Thank you.

Yes, that’s exactly what it is! You can catch a tuple of exceptions and it’ll match anything that matches any of them.

1 Like

Yes, I think this behaviour is not unexpected.

@mmerc was not asking about except but about finally.

When we modify your example and make it into a runnable code:

collection = [{1: None}]

def process(value):
    print(value)

for thing in collection:
    try:
        value = int(thing["value"])
    finally:
        print('Finalization')
        continue
    process(value)

You can check that the exception is not re-raised after finally is executed when the continue command is present. Normally the exception is automatically re-raised after finally is executed.

Is it really the reason? Why would PEP8 discourage from using it if it was designed like that? Anyway, for me it seems like a dirty design if such a side-effect was intentionally added to the commands.

It’s as true of finally as it is of except; if you are able to continue/break/return from those clauses, you cannot also reraise an exception. Since Python does what you request and skips to the next iteration, it clearly cannot also raise the exception (when/where would it??). Older versions of Python had this a SyntaxError. So if it’s permitted at all, the only reasonable thing to do is squelch the exception.

Many things are possible, but are discouraged in the Python standard library. But you’re absolutely welcome to do them in your code, and even in the stdlib, they can be done if the justification is there.

1 Like

By Václav Brožík via Discussions on Python.org at 12Aug2022 21:54:

Is it really the reason?

It’s what those semantics do.

Why would PEP8 discourage from using it if it was designed like that?

There’s plenty of stuff which is discouraged. That just means that you
should avoid it without a good reason. Generally this is so that code
is easy to read and has “common” or “typical” semantics. When you take
the unusual route you’re surprising the reader (or misleading them if
they miss that this is unusual). But sometimes that is what is required.

I discourage you from taking the unusual route without an explainatory
comment :slight_smile:

Anyway, for me it seems like a dirty design if such a side-effect was
intentionally added to the commands.

How would you advocate not reraising the exception which is “live”
during a finally clause? While still allowing the finally to fire?
This is a semi serious question:

  • you want a finally of some kind, which fires even when the code
    inside try/except raises an exception
  • you have a circumstance where that exception should not be reraised

The advantage of putting the control-flow thing in the finally
semantics is that it avoids (error prone and verbose) replication of
“cleanup” or “accounting” code. What’s your alternative proposal?

Cheers,
Cameron Simpson cs@cskk.id.au

By Václav Brožík via Discussions on Python.org at 12Aug2022 21:54:

Anyway, for me it seems like a dirty design if such a side-effect was
intentionally added to the commands.

How would you advocate not reraising the exception which is “live”
during a finally clause? While still allowing the finally to fire?
This is a semi serious question:

  • you want a finally of some kind, which fires even when the code
    inside try/except raises an exception
  • you have a circumstance where that exception should not be reraised

Just wanted to add to this. In some ways the except clause has inverse
semantics to the finally clause because in an except, the “live”
exception is not reraised unless we call raise.

You could therefore advocate that in finally, instead of relying on
break or continue etc to suppress a reraise, a cleaner approach
might be a special form of the raise statement, eg raise None, whose
semantics might be to do some kind of “dummy raise” which escaped the
finally clause itself but then resumed execution below the
try/except/finally`.

To me that feels like what break does in a loop. I’d advocate that for
the “suppress the exception” flow.

But it is already taken:

>>> for i in 1, 2, 3:
...   print(i)
...   try:
...     raise ValueError
...   except ValueError:
...     raise
...   finally:
...     print("finally 1")
...     break
...     print("finally 2")
...
1
finally 1
>>>

Clarifying example:

>>> for i in 1, 2, 3:
...   print(i)
...   try:
...     raise ValueError
...   finally:
...     print("finally 1")
...     break
...     print("finally 2")
...
1
finally 1
>>>

So we can’t use break, as that might break existing code.

I take it that this conflation with the effects on the outer for-loop
control flow is what irks you about the finally no-raise semaantics?

Thoughts?

Cheers,
Cameron Simpson cs@cskk.id.au

By Cameron Simpson via Discussions on Python.org at 12Aug2022 22:59:

So we can’t use break, as that might break existing code.

I take it that this conflation with the effects on the outer for-loop
control flow is what irks you about the finally no-raise semaantics?

I’m rereading the docs here:

particularly the paragraph starting “If finally is present…”.

In some ways I think we’ve got 2 things going on here:

  • clarifying what happens if the programmer puts a break or continue
    or return in a finally clause
  • describing a way to not reraise from finally if there’s a current
    exception

For the former, I think the current semantics are perfectly fine: if the
programme puts one of those control flow statements in a finally
clause, it is not unreasonable to honour it. The programmer knows that
they’re in a finally clause.

For the latter, I almost think we’re misusing these semantics as the
mechanism to not reraise. Because to my eye, that’s the only mechanism
in the finally clause for that.

I was doing the reread to see if the text suggested that the semantics
were for not reraising, or purely to specify what a
break/continue/return in a finally clause meant. I now think the
latter.

So: what should we do to not reraise an exception in the finally
clause?

I think the “finally” approach is backwards; I’ll try to formulate a
clean approach here. Consider: absent one of those control statements, a
finally clause always reraises. Unless there’s an except to
handle the exception.

So probably the clean way to “not reraise” is to identify exceptions
which should not be reraised in excepts only.

try:
  something
except ValueError as e:
  if not funky_ok_value_error(e):
    raise
finally:
  print("something, or something not")

Here we decide whether the ValueError exception should reraise in the
except, and not in the finally.

In fact, the docs state that the finally clause has no access to the
current exception (if any), which also hints that the finally clause
is a bad place to make these decisions. All such decisions belong in the
preceeding except clauses.

Has anyone a counter example where the finally clause is the only
place to reasonably decide about reraise?

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like

Another way to conceptualise it is to think of return/break/continue themselves as “pseudo-exceptions”, that can’t be caught normally. “Raising” any of them cancels any existing exception in progress. Vice versa also applies - you can raise inside a finally, cancelling a return/break/continue.