Finally continuing the discussion over continue in finally
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.
Posted Dec 9, 2024 23:05 UTC (Mon)
by sn403lwn (subscriber, #108883)
[Link]
Thumbs up for the headline, though! I’m sure I’m not the only reader to enjoy that kind of wordplay.
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
Posted Dec 10, 2024 10:59 UTC (Tue)
by taladar (subscriber, #68407)
[Link]
Posted Dec 10, 2024 11:20 UTC (Tue)
by pm215 (subscriber, #98099)
[Link]
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.
Posted Dec 10, 2024 13:24 UTC (Tue)
by epa (subscriber, #39769)
[Link] (9 responses)
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.
Posted Dec 10, 2024 21:05 UTC (Tue)
by Paf (subscriber, #91811)
[Link] (3 responses)
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.
Posted Dec 11, 2024 13:49 UTC (Wed)
by madscientist (subscriber, #16861)
[Link]
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.
Posted Dec 11, 2024 17:42 UTC (Wed)
by epa (subscriber, #39769)
[Link]
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.
Posted Dec 13, 2024 2:05 UTC (Fri)
by NYKevin (subscriber, #129325)
[Link]
Posted Dec 11, 2024 1:34 UTC (Wed)
by roc (subscriber, #30627)
[Link]
Posted Dec 11, 2024 10:00 UTC (Wed)
by taladar (subscriber, #68407)
[Link]
Posted Dec 14, 2024 18:01 UTC (Sat)
by rra (subscriber, #99804)
[Link] (2 responses)
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.
Posted Dec 15, 2024 8:12 UTC (Sun)
by mathstuf (subscriber, #69389)
[Link]
- builds with many warnings don't take umpteen cycles to get quiescence (usually less than a handful)
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.
Posted Dec 16, 2024 10:38 UTC (Mon)
by taladar (subscriber, #68407)
[Link]
Posted Dec 11, 2024 20:03 UTC (Wed)
by amarao (guest, #87073)
[Link] (1 responses)
```
Even now I can't properly argue about the sequence it generates.
Posted Dec 12, 2024 1:32 UTC (Thu)
by JesseW (subscriber, #41816)
[Link]
Posted Dec 12, 2024 4:58 UTC (Thu)
by DimeCadmium (subscriber, #157243)
[Link]
Must... make... more... complex!
Praise for the headline!
Adding warnings in new versions
finally:
@warnings.suppress(warnings.SyntaxWarning)
continue
Adding warnings in new versions
Adding warnings in new versions
Adding warnings in new versions
Adding warnings in new versions
Adding warnings in new versions
Adding warnings in new versions
Adding warnings in new versions
Adding warnings in new versions
Adding warnings in new versions
Adding warnings in new versions
Adding warnings in new versions
- 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
Adding warnings in new versions
add recursion for fun
def foo(x):
try:
foo(x+1)
except:
print(x)
finally:
foo(x)
```
add recursion for fun
I don't like it so it's bad
