Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

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

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

Author:
Irit Katriel <irit at python.org>, Alyssa Coghlan <ncoghlan at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Created:
15-Nov-2024
Python-Version:
3.14
Post-History:
09-Nov-2024,16-Nov-2024
Replaces:
601
Resolution:
Discourse message

Table of Contents

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found atfinally clause.

×

SeePEP 1 for how to propose changes.

Abstract

This PEP proposes to withdraw support forreturn,break andcontinue statements that break out of afinally block.This was proposed in the past byPEP 601. The current PEPis based on empirical evidence regarding the cost/benefit ofthis change, which did not exist at the time thatPEP 601was rejected. It also proposes a slightly different solutionthan that which was proposed byPEP 601.

Motivation

The semantics ofreturn,break andcontinue in afinally block are surprising for many developers.Thedocumentation mentions that:

  • If thefinally clause executes abreak,continueorreturn statement, exceptions are not re-raised.
  • If afinally clause includes areturn statement, thereturned value will be the one from thefinally clause’sreturn statement, not the value from thetry clause’sreturn statement.

Both of these behaviours cause confusion, but the first isparticularly dangerous because a swallowed exception is morelikely to slip through testing, than an incorrect return value.

In 2019,PEP 601 proposed to change Python to emit aSyntaxWarning for a few releases and then turn it into aSyntaxError. It was rejected in favour of viewing thisas a programming style issue, to be handled by linters andPEP 8.Indeed,PEP 8 now recommends not to use control flow statementsin afinally block, and linters such asPylint,Ruff andflake8-bugbearflag them as a problem.

Rationale

A recentanalysis of real world code shows that:

  • These features are rare (2 per million LOC in the top 8,000 PyPIpackages, 4 per million LOC in a random selection of packages).This could be thanks to the linters that flag this pattern.
  • Most of the usages are incorrect, and introduce unintendedexception-swallowing bugs.
  • Code owners are typically receptive to fixing the bugs, andfind that easy to do.

Seethe appendix for more details.

This new data indicates that it would benefit Python’s users ifPython itself moved them away from this harmful feature.

One of the arguments brought up inthe PEP 601 discussionwas that language features should be orthogonal, and combine withoutcontext-based restrictions. However, in the meantimePEP 654 hasbeen implemented, and it forbidsreturn,break andcontinuein anexcept* clause because the semantics of that would violatethe property thatexcept* clauses operatein parallel, so thecode of one clause should not suppress the invocation of another.In that case we accepted that a combination of features can beharmful enough that it makes sense to disallow it.

Specification

The change is to specify as part of the language spec thatPython’s compiler may emit aSyntaxWarning orSyntaxErrorwhen areturn,break orcontinue would transfercontrol flow from within afinally block to a location outsideof it.

These examples may emit aSyntaxWarning orSyntaxError:

deff():try:...finally:return42forxino:try:...finally:break# (or continue)

These examples would not emit the warning or error:

try:...finally:deff():return42try:...finally:forxino:break# (or continue)

CPython will emit aSyntaxWarning in version 3.14, and we leaveit open whether, and when, this will become aSyntaxError.However, we specify here that aSyntaxError is permitted bythe language spec, so that other Python implementations can chooseto implement that.

The CPython implementation will emit theSyntaxWarning duringAST construction, to ensure that the warning will show up duringstatic anlaysis and compilation, but not during execution ofpre-compiled code. We expect that the warning will be seen by aproject maintainer (when they run static analysis, or CI whichdoes not have precompiled files). However, end users of a projectwill only see a warning if they skip precompilation at installationtime, check installation time warnings, or run static analysis overtheir dependencies.

Backwards Compatibility

For backwards compatibility reasons, we are proposing that CPythonemit only aSyntaxWarning, with no concrete plan to upgrade thatto an error. Code running with-We may stop working once thisis introduced.

Security Implications

The warning/error will help programmers avoid some hard to find bugs,so will have a security benefit. We are not aware of security issuesrelated to raising a newSyntaxWarning orSyntaxError.

How to Teach This

The change will be documented in the language spec and in theWhat’s New documentation. TheSyntaxWarning will alert usersthat their code needs to change. Theempirical evidenceshows that the changes necessary are typically quitestraightforward.

Rejected Ideas

EmitSyntaxError in CPython

PEP 601 proposed that CPython would emitSyntaxWarning for a couple ofreleases andSyntaxError afterwards. We are leaving it open whether, andwhen, this will become aSyntaxError in CPython, because we believe that aSyntaxWarning would provide most of the benefit with less risk.

Change Semantics

Itwas suggestedto change the semantics of control flow instructions infinally such that anin-flight exception takes precedence over them. In other words, areturn,break orcontinue would be permitted, and would exit thefinallyblock, but the exception would still be raised.

This was rejected for two reasons. First, it would change the semantics ofworking code in a way that can be hard to debug: afinally that was writtenwith the intention of swallowing all exceptions (correctly using the documentedsemantics) would now allow the exception to propagate on. This may happen onlyin rare edge cases at runtime, and is not guaranteed to be detected in testing.Even if the code is wrong, and has an exception swallowing bug, it could behard for users to understand why a program started raising exceptions in 3.14,while it did not in 3.13.In contrast, aSyntaxWarning is likely to be seen during testing, it wouldpoint to the precise location of the problem in the code, and it would notprevent the program from running.

The second objection was about the proposed semantics. The motivation forallowing control flow statements is not that this would be useful, but ratherthe desire for orthogonality of features (which, as we mentioned in theintroduction, is already violated in the case ofexcept* clauses). However,the proposed semantics are complicated because they suggest thatreturn,break andcontinue behave as they normally do whenfinally executeswithout an in-flight exception, but turn into something like a bareraisewhen there is one. It is hard to claim that the features are orthogonal ifthe presence of one changes the semantics of the other.

Appendix

return infinally considered harmful

Below is an abridged version of aresearch reportby Irit Katriel, which was posted on 9 Nov 2024.It describes an investigation into usage ofreturn,break andcontinuein afinally clause in real world code, addressing thequestions: Are people using it? How often are they using it incorrectly?How much churn would the proposed change create?

Method

The analysis is based on the 8,000 most popular PyPI packages, in terms of numberof downloads in the last 30 days. They were downloaded on the 17th-18th ofOctober, usinga scriptwritten by Guido van Rossum, which in turn relies on Hugo van Kemenade’stool that creates a list of themost popular packages.

Once downloaded, asecond scriptwas used to construct an AST for each file, and traverse it to identifybreak,continue andreturn statements which are directly inside afinally block.

I then found the current source code for each occurrence, and categorized it. Forcases where the code seems incorrect, I created an issue in the project’s bugtracker. The responses to these issues are also part of the data collected inthis investigation.

Results

I decided not to include a list of the incorrect usages, out of concern thatit would make this report look like a shaming exercise. Instead I will describethe results in general terms, but will mention that some of the problems I foundappear in very popular libraries, including a cloud security application.For those so inclined, it should not be hard to replicate my analysis, as Iprovided links to the scripts I used in the Method section.

The projects examined contained a total of 120,964,221 lines of Python code,and among them the script found 203 instances of control flow instructions in afinally block. Most werereturn, a handful werebreak, and none werecontinue. Of these:

  • 46 are correct, and appear in tests that target this pattern as a feature (e.g.,tests for linters that detect it).
  • 8 seem like they could be correct - either intentionally swallowing exceptionsor appearing where an active exception cannot occur. Despite being correct, it isnot hard to rewrite them to avoid the bad pattern, and it would make the codeclearer: deliberately swallowing exceptions can be more explicitly done withexceptBaseException:, andreturn which doesn’t swallow exceptions can bemoved after thefinally block.
  • 149 were clearly incorrect, and can lead to unintended swallowing of exceptions.These are analyzed in the next section.

The Error Cases

Many of the error cases followed this pattern:

try:...exceptSomeSpecificError:...exceptException:logger.log(...)finally:returnsome_value

Code like this is obviously incorrect because it deliberately logs and swallowsException subclasses, while silently swallowingBaseExceptions. The intentionhere is either to allowBaseExceptions to propagate on, or (if the author isunaware of theBaseException issue), to log and swallow all exceptions. However,even if theexceptException was changed toexceptBaseException, this codewould still have the problem that thefinally block swallows all exceptionsraised from within theexcept block, and this is probably not the intention(if it is, that can be made explicit with anothertry-exceptBaseException).

Another variation on the issue found in real code looks like this:

try:...except:returnNotImplementedfinally:returnsome_value

Here the intention seems to be to returnNotImplemented when an exception israised, but thereturn in thefinally block would override the one in theexcept block.

Note

Following thediscussion,I repeated the analysis on a random selection of PyPI packages (toanalyze code written byaverage programmers). The sample containedin total 77,398,892 lines of code with 316 instances ofreturn/break/continueinfinally. So about 4 instances per million lines of code.

Author reactions

Of the 149 incorrect instances ofreturn orbreak in afinally clause,27 were out of date, in the sense that they do not appear in the main/master branchof the library, as the code has been deleted or fixed by now. The remaining 122are in 73 different packages, and I created an issue in each one to alert theauthors to the problems. Within two weeks, 40 of the 73 issues received a reactionfrom the code maintainers:

  • 15 issues had a PR opened to fix the problem.
  • 20 received reactions acknowledging the problem as one worth looking into.
  • 3 replied that the code is no longer maintained so this won’t be fixed.
  • 2 closed the issue as “works as intended”, one said that they intend toswallow all exceptions, but the other seemed unaware of the distinctionbetweenException andBaseException.

One issue was linked to a pre-existing open issue about non-responsiveness to Ctrl-C,conjecturing a connection.

Two of the issue were labelled as “good first issue”.

The correct usages

The 8 cases where the feature appears to be used correctly (in non-test code) alsodeserve attention. These represent the “churn” that would be caused by blockingthe feature, because this is where working code will need to change. I did notcontact the authors in these cases, so we need to assess the difficulty ofmaking these changes ourselves. It is shown inthe full report,that the change required in each case is small.

Discussion

The first thing to note is thatreturn/break/continue in afinallyblock is not something we see often: 203 instance in over 120 million linesof code. This is, possibly, thanks to the linters that warn about this.

The second observation is that most of the usages were incorrect: 73% in oursample (149 of 203).

Finally, the author responses were overwhelmingly positive. Of the 40 responsesreceived within two weeks, 35 acknowledged the issue, 15 of which also createda PR to fix it. Only two thought that the code is fine as it is, and threestated that the code is no longer maintained so they will not look into it.

The 8 instances where the code seems to work as intended, are not hard torewrite.

Copyright

This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.


Source:https://github.com/python/peps/blob/main/peps/pep-0765.rst

Last modified:2025-03-20 10:09:14 GMT


[8]ページ先頭

©2009-2026 Movatter.jp