|
|
Subscribe / Log in / New account

Finally continuing the discussion over continue in finally

By Daroc Alden
December 9, 2024

In 2019, the Python community had a lengthy discussion about changing the rules (that some find counterintuitive) on using break, continue, or return statements in finally blocks. These are all ways of jumping out of a finally block, which can interrupt the handling of a raised exception. At the time, the Python developers chose not to change things, because the consensus was that the existing behavior was not a problem. Now, after a report put together by Irit Katriel, the project is once again considering changing the language.

Like several other languages, Python has a try statement that allows catching exceptions. The optional finally block of a try statement allows the programmer to write code that should always run, regardless of whether an exception occurred in the try statement. This facility is frequently used to ensure a resource is always cleaned up, even if an exception is thrown. The Python documentation clearly describes what happens when an exception is thrown and a finally block that includes a control-flow statement executes: "If the finally clause executes a return, break or continue statement, the saved exception is discarded". But some people see this behavior as counterintuitive.

When Batuhan Taskaya originally proposed PEP 601 ("Forbid return/break/continue breaking out of finally"), forbidding control-flow statements in finally blocks, they called the behavior "not at all obvious". At the time, a handful of other Python developers agreed. Brandt Bucher shared an example of one of the kinds of potentially surprising code that is currently allowed:

    >>> def f() -> bool:
    ...     while True:
    ...         try:
    ...             return True
    ...         finally:
    ...             break
    ...     return False
    ...
    >>> f()
    False

This code would normally exit from f() upon reaching the return statement — except that the finally block runs and breaks out of the enclosing loop, letting execution continue into the rest of the function. Therefore the value returned from inside the try block is lost, and the function returns False. A similar example that throws an exception from inside the try block would show that the exception is lost as well.

Other people did not think that this behavior was a problem. Paul Moore called the motivation for the proposal weak, and asked whether there were any real-world examples of people using control-flow statements inside a finally block incorrectly. Serhiy Storchaka supplied two examples that they thought were bugs in the standard library — but Guido van Rossum indicated that only one of those was actually a bug.

Ultimately, the Python steering council voted unanimously not to adopt PEP 601, but suggested that the rule should instead be added to PEP 8 ("Style Guide for Python Code") as a matter of good coding practice.

Revisiting the topic

In November 2024, Katriel reopened the discussion by posting a link to a report that examines the 8,000 most popular Python packages. Katriel wrote a script to examine nearly 121 million lines of Python code. The script found 203 cases where there were control-flow statements in a finally block that could cause the block to be exited in a way that could potentially suppress exceptions. She examined the examples and determined that 46 were correct, 149 were clearly incorrect and 8 were difficult to classify. Notably, almost all of the correct uses appear in tests for linters that check that the linter can identify when a control-flow statement is used like this.

Katriel analyzed the incorrect cases in more detail, noting that 27 of them had actually been fixed in the development branch of the relevant library. She reported the remaining cases to the developers of affected packages. Of the 73 packages where she opened issues, only two indicated that the code was working as intended. With these facts in hand, Katriel and Alyssa Coghlan proposed PEP 765 ("Disallow return/break/continue that exit a finally block").

The new PEP would issue a SyntaxWarning for any use of a return, break, or continue statement that would exit a finally block. The PEP would essentially call for the Python interpreter to warn about any means of exiting a finally block other than letting control flow reach the end of the block normally. This would be a syntax warning so that it shows up when Python code is first parsed, not when the code is actually executed. This means that the warning would appear to library authors, but not to most library users, who usually use pre-compiled Python bytecode. These users might see the warning at installation time, but not while running their Python scripts.

Van Rossum called the report "an excellent piece of research", and suggested that "perhaps we should do more PEPs based on such investigations". He was, however, "personally sad to see this syntactic corner case disappear". He believes that having control-flow statements within finally blocks which work as they do now is an example of how Python has many small features that can be seamlessly composed together.

Katriel responded that there are already similar exceptions to the rules in Python. Specifically, except* clauses already disallow control-flow statements. The overall response to the proposal was fairly positive, however. Several people chimed in to express their support, and Tim Peters, recently back from his three-month suspension, went so far as to call the existing behavior "a bug magnet on the face of it".

Not everyone was as convinced by Katriel's research. Robin Becker thought that the existing behavior was obvious, and said in a later message that there was nothing special about finally blocks suppressing exceptions. Peters disagreed, quoting the Zen of Python, which he authored:

Errors should never pass silently.
Unless explicitly silenced.

That led Steve Dower to propose an alternative: "why are we forbidding the construct rather than just making exceptions re-raise on exit from finally, regardless of how we leave the block?" He did later clarify that he wasn't really arguing for that alternative, but just wanted an explanation of why the PEP didn't seem to consider the possibility.

Katriel said that Dower's proposal would introduce backward-compatibility problems; code that was previously correct would raise unexpected exceptions. Dower wasn't satisfied with that explanation, saying that adding a syntax warning would also disrupt existing code. He eventually asked: "So is it better if the error is still raised? Or better if everyone who uses your module gets a warning (that breaks their own tests/users) whether that error ever occurs or not?"

Katriel thought the latter was preferable, because it is easier to debug a syntax warning that points to the exact line causing the issue, rather than a random no-longer-suppressed exception that may occur without warning. In the end, neither Katriel nor Dower was able to convince the other, but Katriel did add a section to the PEP summarizing Dower's proposed alternative and why she did not think it was a good idea. Ultimately, Dower wanted a solution that "minimises breakage", since he is going to have to justify any changes to his users, who are already "unjustifiably nervous" about updating to Python 3.12.

Daniël van Noord asked Dower what it would take for him to support the PEP. Dower replied that Python has "many millions of users and billions of lines of code" — so adding a new warning is going to impact people, no matter how rare the circumstances. He won't support a change unless that can somehow be avoided.

Katriel pointed out that the choice to issue a warning instead of an error was deliberate — existing Python code would continue to work, just possibly with a warning. That led to an extended discussion about under which circumstances the warning would be shown, whether that was too disruptive, and various related concerns. Several people said that they thought putting a control-flow statement inside a finally block should be an error, instead of a warning, despite that being more backward incompatible than Katriel's proposed change.

Ultimately, Katriel submitted the PEP to the Python Steering Council. So regardless of the arguments in favor (the fact that the feature is almost never used correctly) and the arguments against (any amount of backward incompatibility is a pain), it will lie with the council to decide whether the PEP is accepted. The council faces yearly elections — and being so close to the end of its term, it decided to leave the decision to next year's council. (The election for which ends on December 9.) Van Rossum, near the end of the discussion, said that he has mixed feelings about the proposal. "I honestly don't know what I would have done when I was BDFL [benevolent dictator for life]." What the steering council will decide remains to be seen, but it seems clear that either choice is going to make some people unhappy.



to post comments

Praise for the headline!

Posted Dec 9, 2024 23:05 UTC (Mon) by sn403lwn (subscriber, #108883) [Link]

That’s an interesting story about issues in control flow, about which I do not have strong opinions.

Thumbs up for the headline, though! I’m sure I’m not the only reader to enjoy that kind of wordplay.

Adding warnings in new versions

Posted Dec 10, 2024 7:56 UTC (Tue) by chris_se (subscriber, #99706) [Link] (12 responses)

I personally don't have a strong opinion on what the best solution for the control-flow logic is (I personally think this is a hard question to answer when it comes to language design), but I find the claim that adding a warning "causes breakage" to be entirely unconvincing. I mostly work with C++ in my day job and newer compiler versions always add additional warnings that better lint the code for potential issues. As long as that new warning doesn't actually change the behavior of the code I really don't get why this would be a huge concern and intimidating to users.

That said, C++ compilers do offer the option to suppress warnings in cases where people say they know what they are doing, and while you can suppress warnings in Python, you can't really do that with SyntaxWarning as far as I am aware, because that happens before the code is executed. (Well, you could suppress them during import, but that kind of defeats the purpose.) Perhaps this could be done via a decorator like

finally:
    @warnings.suppress(warnings.SyntaxWarning)
    continue

Adding warnings in new versions

Posted Dec 10, 2024 10:59 UTC (Tue) by taladar (subscriber, #68407) [Link]

I find that argument that a warning causes breakage particularly unconvincing in the presence of a comprehensive study that shows that a vast majority of uses (outside of linter tests) are incorrect. If anything I would consider that evidence that something stronger than a warning is warranted.

Adding warnings in new versions

Posted Dec 10, 2024 11:20 UTC (Tue) by pm215 (subscriber, #98099) [Link]

I think the difference with C++ is that the warnings are generated at compile time, when there's always a developer around who can act on them. So you can build your code, and fix the warnings or not, as you choose -- but either way, when you deploy the final binary your users are not going to see these warnings. With Python, even though a SyntaxWarning only appears the first time python code is byte-compiled, it's still possible or plausible that end-users who just want to use a program may get warnings, and people using a Python library may get warnings about problems in the library that they cannot fix, because "autogenerate the bytecode on demand" is kind of the intended flow.

This seems (from my reading of the comment thread) to be why Steve Dower would prefer to make it a linter error -- because the linter is a tool run by the developer on their code in advance, not something that might happen at runtime when the developer is nowhere in sight.

Adding warnings in new versions

Posted Dec 10, 2024 13:24 UTC (Tue) by epa (subscriber, #39769) [Link] (9 responses)

Sadly there are some people who treat warnings as errors. Flags like gcc's -Werror have their place but should not be the standard way to build your program. Otherwise you're not programming C++ but some ever-shifting subset depending on what warnings the latest compiler emits. (It's fine for the developer's personal build to be extra-strict, but not to impose this downstream.)

Similarly, if you run a subprocess and treat any output on stderr as failure then you no longer have a distinction between warnings (which you probably want to look at, but shouldn't cause everything to break right now) and errors (which are severe enough that processing should not continue unless the error is explicitly handled).

It's true that an excessively verbose runtime warning could bloat your log files and make it hard to see what's going on. But that doesn't really apply to syntax warnings, which appear only when the interpreter starts up and the program is compiled.

Adding warnings in new versions

Posted Dec 10, 2024 21:05 UTC (Tue) by Paf (subscriber, #91811) [Link] (3 responses)

"Sadly there are some people who treat warnings as errors. Flags like gcc's -Werror have their place but should not be the standard way to build your program. Otherwise you're not programming C++ but some ever-shifting subset depending on what warnings the latest compiler emits. (It's fine for the developer's personal build to be extra-strict, but not to impose this downstream.)"

It's fine for projects to do this - it is their choice what level of strictness they wish to apply to their codebase. My project finds warn-as-error extremely helpful in reducing weird constructs - correct or not - and catching the occasional bug. That's our choice as a project, and we've decided the occasional silly compiler silencing dance is worth it.

I guess my point is projects can make this choice just as much as individuals and for exactly the same reasons. They get to pick their approach to "clean" builds, etc.

Adding warnings in new versions

Posted Dec 11, 2024 13:49 UTC (Wed) by madscientist (subscriber, #16861) [Link]

It's a good thing for projects, of any type, to set -Werror for their own builds. The last thing you want is for your "normal" builds to be spewing tons of warnings that then no one pays any attention to. Fix these early and aggressively, before code is pushed to the main branch.

It's a bad thing for FOSS project to ship their source code packages with -Werror enabled by default for other peoples' builds. This just means that the source fails to compile for, usually, fairly innocuous reasons on users' systems in ways they probably (unless they're programmers themselves) can't fix, and generates a lot of extra work for the maintainers and authors.

Adding warnings in new versions

Posted Dec 11, 2024 17:42 UTC (Wed) by epa (subscriber, #39769) [Link]

That's true only if the project fixes on a particular exact version of a particular compiler. If you use -Werror then downstream users who are building from source, or packagers for Linux distributions, will have a hard time using a different compiler version. Even if the only change is that their newer version added a warning-level diagnostic.

So yes, it's the project's choice, but they should be aware of the consequence of that choice and explictly declare that a particular compiler version, emitting a particular set of warnings, is required to build the program. Personally, I would think that is imposing too much work on downstream if you want your program to be generally adopted.

Adding warnings in new versions

Posted Dec 13, 2024 2:05 UTC (Fri) by NYKevin (subscriber, #129325) [Link]

It is fine for projects to use -Werror, but it is also fine for compilers to introduce new warnings without any deprecation period. Then it's up to the project to decide what they want to do about the resulting warnings-converted-to-errors. The average compiler will (probably) have little sympathy if you try to make it their problem.

Adding warnings in new versions

Posted Dec 11, 2024 1:34 UTC (Wed) by roc (subscriber, #30627) [Link]

For rr we have debug builds use -Werror and release builds not, on the assumption that the former is mostly people hacking on rr and the latter are mostly users who just want to build it.

Adding warnings in new versions

Posted Dec 11, 2024 10:00 UTC (Wed) by taladar (subscriber, #68407) [Link]

Honestly, a lot of the time treating warnings as errors is a workaround for compilers treating things that really should be an error as a warning because someone else complained that treating it as an error would break their existing code base.

Adding warnings in new versions

Posted Dec 14, 2024 18:01 UTC (Sat) by rra (subscriber, #99804) [Link] (2 responses)

> Sadly there are some people who treat warnings as errors.

Speaking as one of those people, I wish everyone on all sides of these discussions would recognize that there is a social contract involved in doing this. As a developer who treats warnings as errors in my CI builds, I make that decision with the full knowledge that this means my builds will periodically break due to new warnings and I will need to go fix them. This is the behavior I want (I like continuous code improvement), which is why I chose that approach. If I don't want that obligation, then I shouldn't treat warnings as errors.

Whenever this topic comes up, there seems to be some amount of people wanting to have it both ways: the benefits of warnings, without the obligation to constantly change your code to keep it warning-free. This is not how the world works. You get to pick one or the other. You can mitigate the impact by only using warnings for development and not for release builds, but at some point you have to pick whether you want the benefits and the pain of warnings, or neither.

Perhaps the most frustrating pattern (and to be clear, I am not saying you are doing this, only that I have seen it elsewhere) is when people who don't like new warnings and therefore do not treat warnings as errors also argue against adding new warnings (that they personally don't see) on the grounds that other people treat warnings as errors. There really are two loose schools of thought about this, and part of the friction is that the two schools don't leave each other alone. I want new warnings to be added. I know that means new maintenance work for me. That's the point. This is the social contract that I am explicitly accepting.

Adding warnings in new versions

Posted Dec 15, 2024 8:12 UTC (Sun) by mathstuf (subscriber, #69389) [Link]

There's a third way that we use in most of our projects: detect warnings at a higher level and fail the *job*, not the *build* when they show up. Benefits that I've found:

- builds with many warnings don't take umpteen cycles to get quiescence (usually less than a handful)
- ability to ignore warnings based on path (managing flags on such a level is…very obnoxious)
- ignore false positive warnings (GCC seems to have had a number around memcpy/memmove bounds lately)
- the return code can be "special" and allow the pipeline to also run tests on your warning-filled build while blocking merging

AFAIK, this is not possible with GHA as there is no way to specify "run if dependent has a given exit code". There's also no "failed, but allowed to fail" state to visually distinguish this state in the web UI.

Of course, if one doesn't have something that can do higher-level diagnostic management it's harder to build. We use CTest's ability to watch the output of the build, but I think that once SARIF becomes more widespread, it is likely to be a more reliable medium for doing this kind of filtering.

Adding warnings in new versions

Posted Dec 16, 2024 10:38 UTC (Mon) by taladar (subscriber, #68407) [Link]

To me the most annoying variant is when people convince compiler makers to make something a warning instead of an error as a "compromise" even though it clearly should be one because they want to be able to ignore it in their existing code bases.

add recursion for fun

Posted Dec 11, 2024 20:03 UTC (Wed) by amarao (guest, #87073) [Link] (1 responses)

Many years ago I found this peculiar thing and wrote this abysmal thing:

```
def foo(x):
try:
foo(x+1)
except:
print(x)
finally:
foo(x)
```

Even now I can't properly argue about the sequence it generates.

add recursion for fun

Posted Dec 12, 2024 1:32 UTC (Thu) by JesseW (subscriber, #41816) [Link]

That is ... disgusting. Congratulations. Thank you for sharing it.

I don't like it so it's bad

Posted Dec 12, 2024 4:58 UTC (Thu) by DimeCadmium (subscriber, #157243) [Link]

> In mosaicml there is a return in a finally at the end of the main function, after an except: clause which swallows all exceptions. The return in the finally would swallow an exception raised from within the except: clause, but this seems to be the intention. A possible fix would be to assign the return value to a variable in the finally clause, dedent the return statement and wrap the body of the except: clause by another try-except that would swallow exceptions from it.

Must... make... more... complex!


Copyright © 2024, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds