245 lines
7.6 KiB
Plaintext
245 lines
7.6 KiB
Plaintext
PEP: 601
|
||
Title: Forbid return/break/continue breaking out of finally
|
||
Author: Damien George, Batuhan Taskaya
|
||
Sponsor: Nick Coghlan
|
||
Discussions-To: https://discuss.python.org/t/pep-601-forbid-return-break-continue-breaking-out-of-finally/2239
|
||
Status: Draft
|
||
Type: Standards Track
|
||
Content-Type: text/x-rst
|
||
Created: 26-Aug-2019
|
||
Python-Version: 3.8
|
||
Post-History: 26-Aug-2019
|
||
|
||
Abstract
|
||
========
|
||
|
||
This PEP proposes to forbid return, break and continue statements within
|
||
a finally suite where they would break out of the finally. Their use in
|
||
such a location silently cancels any active exception being raised through
|
||
the finally, leading to unclear code and possible bugs.
|
||
|
||
Continue is currently not supported in a finally in Python 3.7 (due to
|
||
implementation issues) and the proposal is to not add support for it in
|
||
Python 3.8. For return and break the proposal is to deprecate their use
|
||
in Python 3.9, emit a compilation warning in Python 3.10 and then forbid
|
||
their use after that.
|
||
|
||
Motivation
|
||
==========
|
||
|
||
The use of return, break and continue within a finally suite leads to behaviour
|
||
which is not at all obvious. Consider the following function::
|
||
|
||
def foo():
|
||
try:
|
||
foo()
|
||
finally:
|
||
return
|
||
|
||
This will return cleanly (without an exception) even though it has infinite
|
||
recursion and raises an exception within the try. The reason is that the return
|
||
within the finally will silently cancel any exception that propagates through
|
||
the finally suite. Such behaviour is unexpected and not at all obvious.
|
||
This function is equivalent to::
|
||
|
||
def foo():
|
||
try:
|
||
foo()
|
||
except:
|
||
pass
|
||
return
|
||
|
||
Break and continue have similar behaviour (they silence exceptions) if they
|
||
jump to code outside the finally suite. For example::
|
||
|
||
def bar():
|
||
while True:
|
||
try:
|
||
1 / 0
|
||
finally:
|
||
break
|
||
|
||
This behaviour goes against the following parts of The Zen of Python:
|
||
|
||
* Explicit is better than implicit - exceptions are implicitly silenced
|
||
|
||
* Readability counts - the intention of the code is not obvious
|
||
|
||
* Errors should never pass silently; Unless explicitly silenced - exceptions
|
||
are implicitly silenced
|
||
|
||
If this behaviour of silencing exceptions is really needed then the explicit
|
||
form of a try-except can be used instead, and this makes the code clearer.
|
||
|
||
Independent to the semantics, implementing return/break/continue within a
|
||
finally suite is non-trivial as it requires to correctly track any active
|
||
exceptions at runtime (an executing finally suite may or may not have an
|
||
active exception) and cancel them as appropriate. CPython did have a bug in
|
||
this for the case of continue and so originally disallowed it [1]. Requiring
|
||
correct behaviour for return/break/continue within a finally puts an
|
||
unnecessary burden on alternative implementations of Python.
|
||
|
||
Other languages
|
||
===============
|
||
|
||
Java allows to return from within a finally block, but its use is discouraged
|
||
according to [2], [3], [4]. The Java compiler later on included a linting
|
||
option -Xlint:finally to warn against the use of return within a finally block.
|
||
The Eclipse editor also warns about this use.
|
||
|
||
Ruby allows return from inside ensure (Python's finally), but it should be an
|
||
explicit return. It is discouraged and handled by linters [5], [6].
|
||
|
||
Like Ruby, JavaScript also allows use of return/break/continue within a finally
|
||
but it is seen as unsafe and it is handled by eslint [7].
|
||
|
||
C# forbids the use of ending statements like return/goto/break within a finally
|
||
[8], [9].
|
||
|
||
Rationale
|
||
=========
|
||
|
||
Since the behaviour of return/break/continue within a finally is unclear, the
|
||
pattern is rarely used, and there is a simple alternative to writing equivalent
|
||
code (which is more explicit), forbidding the syntax is the most straightforward
|
||
approach.
|
||
|
||
Specification
|
||
=============
|
||
|
||
This is a change to the compiler, not the grammar. The compiler should
|
||
check for the following in a finally suite:
|
||
|
||
* A return in any statement, at any level of nesting.
|
||
|
||
* A break/continue in any statement, at any level of nesting, that would
|
||
transfer control flow outside the finally suite.
|
||
|
||
Upon finding such a case it should emit the appropriate exception:
|
||
|
||
* For continue, a SyntaxError (this is the current behaviour of 3.7).
|
||
|
||
* For return/break, a SyntaxWarning in 3.10, and a SyntaxError after that.
|
||
|
||
For example, the following are all forbidden by this proposal::
|
||
|
||
def f():
|
||
try:
|
||
pass
|
||
finally:
|
||
return
|
||
|
||
def g():
|
||
try:
|
||
pass
|
||
finally:
|
||
try:
|
||
return
|
||
finally:
|
||
pass
|
||
|
||
def h():
|
||
try:
|
||
pass
|
||
finally:
|
||
try:
|
||
pass
|
||
finally:
|
||
for x in range(10):
|
||
return
|
||
|
||
The following is still allowed because the continue doesn't escape the
|
||
finally::
|
||
|
||
try:
|
||
pass
|
||
finally:
|
||
for x in range(10):
|
||
continue
|
||
|
||
Note that yielding from within a finally remains acceptable by this PEP
|
||
because resuming the generator will resume the finally and eventually
|
||
raise any active exceptions (so they are never silenced by yielding).
|
||
|
||
Backwards Compatibility
|
||
=======================
|
||
|
||
This is a backwards incompatible change, for return and break.
|
||
|
||
The following locations in the CPython standard library (at
|
||
v3.8.0b1-651-g7fcc2088a5) use return within finally:
|
||
|
||
* Lib/subprocess.py:921 - the use here looks like a bug
|
||
|
||
* Lib/multiprocessing/connection.py:316 - the use here looks legitimate
|
||
but the intention is not clear
|
||
|
||
* Lib/multiprocessing/connection.py:318 - the use here looks legitimate
|
||
but the intention is not clear
|
||
|
||
* Lib/test/test_sys_settrace.py:837 - a test for return within finally
|
||
|
||
* Lib/test/test_sys_settrace.py:1346 - a test for return within finally
|
||
|
||
There are no uses of break within a finally (that break out of the finally)
|
||
in the standard library.
|
||
|
||
Security Implications
|
||
=====================
|
||
|
||
This is a simplification of the language, and removal of associated code,
|
||
so should not introduce any new paths for a security exploit.
|
||
|
||
How to Teach This
|
||
=================
|
||
|
||
This feature is very rarely used so forbidding it will likely only impact
|
||
advanced users, not beginners and probably not any existing teaching
|
||
material. Since this is the removal of a feature teaching users will be
|
||
one by the raising of a SyntaxError if/when the forbidden feature is used.
|
||
|
||
Reference Implementation
|
||
========================
|
||
|
||
There is currently no reference implementation, although the way continue
|
||
is currently handled in a finally (raising a SyntaxError) can be extended
|
||
to return and break.
|
||
|
||
References
|
||
==========
|
||
|
||
.. [1] https://bugs.python.org/issue37830
|
||
|
||
.. [2] https://stackoverflow.com/questions/48088/returning-from-a-finally-block-in-java
|
||
|
||
.. [3] https://web.archive.org/web/20070922061412/http://weblogs.java.net/blog/staufferjames/archive/2007/06/_dont_return_in.html
|
||
|
||
.. [4] https://wiki.sei.cmu.edu/confluence/display/java/ERR04-J.+Do+not+complete+abruptly+from+a+finally+block
|
||
|
||
.. [5] https://github.com/rubocop-hq/rubocop/issues/5949
|
||
|
||
.. [6] https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Lint/EnsureReturn
|
||
|
||
.. [7] https://eslint.org/docs/rules/no-unsafe-finally
|
||
|
||
.. [8] https://social.msdn.microsoft.com/Forums/vstudio/en-US/87faf259-3c54-4f3a-8d2b-ff82de44992f/return-statement-in-finally-block?forum=netfxbcl
|
||
|
||
.. [9] https://stackoverflow.com/a/5788268
|
||
|
||
Copyright
|
||
=========
|
||
|
||
This document is placed in the public domain or under the
|
||
CC0-1.0-Universal license, whichever is more permissive.
|
||
|
||
|
||
|
||
..
|
||
Local Variables:
|
||
mode: indented-text
|
||
indent-tabs-mode: nil
|
||
sentence-end-double-space: t
|
||
fill-column: 70
|
||
coding: utf-8
|
||
End:
|