PEP 765: Disallow return/break/continue that exit a finally block

Yes: 3.14.0 candidate 2 is scheduled for the 26th.

A

I believe that the instance you pointed to actually does contain an error:

            try:
                f.result()
            except Exception:
                _log.exception('An error occurred while stopping the gateway. Ignoring.')
            finally:
                self.stop()
                return

It’s unlikely that the author intended to log-and-swallow Exceptions while silently swallowing BaseExceptions.

This was evaluated before, and the most recent re-analysis that happened as a result of recent things is linked. It is both intentional, and the base exceptions that could be raised there are extremely limited due to it being in a library managed thread.

Why do warnings break CI? Is there no difference between a warning and an error?

It is not uncommon to run pytest -Werror so that warnings produced by changes in upstream packages can be addressed before they become user-facing problems. In general, the response is to look at the warning, make any necessary changes and suppress it if necessary.

4 Likes

As already explained, warnings as errors are pretty common, usually to catch real problems. However, this is the first time that I’m aware of in python that there is a warning for something that is not unambiguously incorrect or planned to become disallowed, and which does not obey python’s warning filter mechanisms (which many test runners defer to, as it is more powerful than anything accomplishable by parsing stderr/stdout) This makes the normal option people using this expect (filtering a warning they are aware of and does not effect them, rather than pushing for a new release of a dependency) not an option.

Maybe we need a proper warning mechanism then, for things that are not intended to be breaking.

I think we already have a proper warning mechanism. If people choose to run with warnings as errors, they’re explicitly opting into the consequences.

But I do think we can do better specifically with SyntaxWarnings. The problem with them is that they only get triggered during bytecode compilation, so they won’t appear again if you happen to already have a pyc file generated. That can be confusing and makes it harder to test:

$ cat pep765.py 
def f():
    try:
        1/0
    finally:
        return 42
$ cat caller.py 
import pep765
$ ./python.exe caller.py 
/Users/jelle/py/cpython/pep765.py:5: SyntaxWarning: 'return' in a 'finally' block
  return 42
$ ./python.exe caller.py 
$

Maybe as a solution, we could record any warnings that were raised during compilation in the pyc file and raise them again if we load the pyc from cache?

8 Likes

I’m fine with the current warning mechanism, the problem I have in this case is this specific warning doesn’t obey warning filters (because the location info is wrong for syntax warnings; There’s an open PR for this), which is what people who use warnings as errors use to go “we know about this one, it’s fine”

5 Likes

Not getting the warning at runtime was intentional (and cited as a mitigating factor in the backwards compatibility section of the PEP).

Reliably detecting syntax warnings is a task for linters and type checkers.

1 Like

The finally block can be split on the bare except and else blocks with identical content.

try:
    foo()
finally:
    bar()

is equivalent to

try:
    foo()
except:
    bar()
else:
    bar()

So if you have a syntax warning about return/break/continue in the finally block, just rewrite it as above. This will remove warning. And then you can see if there was an error in your code, if return/break/continue in the except block was used incorrectly (silently swallowing all exceptions).

There is a pitfall – if there are except clauses together with the finally clause in the same try statement, it is actually a merge of the try/finally statement and nested try/except statement. They should be unmerged before performing the above decomposition.

2 Likes

I get that this is intentional, but I don’t think the current situation is good for users.

Suppose you run a CI step (likely your test suite) that treats warnings as errors. That’s generally something we should encourage: people should pay attention to warnings in their code and address them if possible. Now, you use a third-party package that emits a SyntaxWarning. Whether your test suite will fail might depend on whether you use pip (which pre-compiles installed packages by default) or uv (which doesn’t). It might also fail on CI (where you likely run a fresh environment with no pre-existing pyc files) but succeed in your local dev environment (where you probably ran something before that generated the pyc file). In your local environment, you’ll probably have had the warning printed at some point, but you might not have seen it (some programs print a lot of output), and if you missed it the one time the pyc was generated, you’re out of luck.

Reliably detecting syntax warnings is a task for linters and type checkers.

Sure, linters can detect these conditions (and much more), but if people should rely on linters to catch these issues, what’s even the point of adding these warnings to CPython?

7 Likes

That’s not what I said. I said people should rely on linters to reliably catch them: if someone wants full assurance of zero syntax warnings in all of their code, but they aren’t running a linter or type checker, then changing that is the place to start.

The relatively small number of constructs that we consider problematic enough to emit syntax warnings for, but not so problematic that we treat them as outright syntax errors, are there so folks get the warning for code they are actively changing, without getting it for code they are merely running. If users aren’t changing it, any syntax warnings aren’t their problem (and if they want them to be their problem, they can run a linter or type checker).

Edit: tweaked pronouns, since the distinction between “we” as core developers and “we” as general Python users was thoroughly unclear.

I wonder if we all made a mistake here. I was skeptical when this was first proposed. The semantics are unambiguous and useful. The “solution” seems to cause more problems, plus pushback from some respected folks.

Surely if we write a PEP to revert this in 3.14.1 we can leave all that behind us.

12 Likes

But the syntax was misunderstood by most of the users who did use it (as the research by @iritkatriel showed), resulting in one of the most nasty kinds of bugs: unintended silent suppression of an exception [1]. At the same time, the behavior, when really needed (which seems a rare case), can be achieved in a clearer (and still simple) way, as @storchaka pointed out above.

I believe that the actual issue this discussion thread highlights is to find a way to make it possible to selectively (per library path?) filter SyntaxWarnings during compilation of *.pyc files (a new command-line switch? a new environment variable?).


  1. I must admit that when I first learned what the actual semantics were, I was very surprised – as my intuition was that while the statements in (the outermost) finally block had the “last word” about the normal control flow (whether to return/continue/break, what shall be returned etc.), everything they could do to the exceptional control flow was either to defer the inevitable propagation of the exception by executing a few statements before it happens, or to raise a new exception (replacing the one being propagated). That is, I was surprised that a mere return-like statement (a normal one) could cancel an exception. ↩︎

2 Likes

That does not mean that a SyntaxWarning is a helpful way of addressing the problem though. The research makes a good case for saying that this should be a linting rule that is enabled by default in the most widely used linting tools.

7 Likes

If someone wants to propose this PEP and the SC accepts it, then that’s what we’ll do.

The consequence of that will probably be that nobody will ever introduce a new SyntaxWarning ever again (I certainly won’t). So that would be a PEP to effectively deprecate the warning system for compile time issues.

7 Likes

I think it’s more subtle than that. Issuing SyntaxWarning about deprecated syntax that will go away in a few releases feels like a good use of warnings (same as runtime warnings). But in this case the decision was made not to deprecate the questionable feature, and that is (from what I can understand) problematic for people who make legitimate, non-buggy use of the feature.

So the new PEP (if someone volunteers to write one) should probably deprecate the latter but not the former (and it should call out PEP 765 and suggest to repeal it).

I have no idea how the SC feels about all this though.

3 Likes

This is not my understanding. The intention is to remove the feature from the language (to signal that it should not be used). It should be a SyntaxError, it’s not only because that would be disruptive for a small number of cases where people think that their use case is legitimate and not buggy (although it reads like a bug). The concession of using a Warning rather than Error didn’t work because people use -We so Warnings are Errors. And, evidently, it sent the unintended message that this is not a serious issue, since we didn’t say when it becomes an Error.

4 Likes

To provide a bit of a counterpoint to the handful of people who are passionate enough about this to keep beating this drum: To me, removing this footgun is absolutely the right direction. Insisting that the footgun should stay because you know how to use it is an understandable reaction, but not in line with the broader interests of the ecosystem.

I think the most effective way forward would be to ensure that the warning can be filtered correctly. But ultimately that code should be restructured in anticipation of eventual removal of the footgun.

20 Likes