Skip to content

gh-130080: implement PEP 765 #130087

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 22 additions & 18 deletions Doc/reference/compound_stmts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -420,16 +420,16 @@ is executed. If there is a saved exception it is re-raised at the end of the
:keyword:`!finally` clause. If the :keyword:`!finally` clause raises another
exception, the saved exception is set as the context of the new exception.
If the :keyword:`!finally` clause executes a :keyword:`return`, :keyword:`break`
or :keyword:`continue` statement, the saved exception is discarded::
or :keyword:`continue` statement, the saved exception is discarded. For example,
this function returns 42.

>>> def f():
... try:
... 1/0
... finally:
... return 42
...
>>> f()
42
.. code-block::

def f():
try:
1/0
finally:
return 42

The exception information is not available to the program during execution of
the :keyword:`!finally` clause.
Expand All @@ -446,21 +446,25 @@ statement, the :keyword:`!finally` clause is also executed 'on the way out.'
The return value of a function is determined by the last :keyword:`return`
statement executed. Since the :keyword:`!finally` clause always executes, a
:keyword:`!return` statement executed in the :keyword:`!finally` clause will
always be the last one executed::
always be the last one executed. The following function returns 'finally'.

>>> def foo():
... try:
... return 'try'
... finally:
... return 'finally'
...
>>> foo()
'finally'
.. code-block::

def foo():
try:
return 'try'
finally:
return 'finally'

.. versionchanged:: 3.8
Prior to Python 3.8, a :keyword:`continue` statement was illegal in the
:keyword:`!finally` clause due to a problem with the implementation.

.. versionchanged:: next
The compiler emits a :exc:`SyntaxWarning` when a :keyword:`return`,
:keyword:`break` or :keyword:`continue` appears in a :keyword:`!finally`
block (see :pep:`765`).


.. _with:
.. _as:
Expand Down
8 changes: 6 additions & 2 deletions Doc/tutorial/errors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,9 @@ points discuss more complex cases when an exception occurs:

* If the :keyword:`!finally` clause executes a :keyword:`break`,
:keyword:`continue` or :keyword:`return` statement, exceptions are not
re-raised.
re-raised. This can be confusing and is therefore discouraged. From
version 3.14 the compiler emits a :exc:`SyntaxWarning` for it
(see :pep:`765`).

* If the :keyword:`!try` statement reaches a :keyword:`break`,
:keyword:`continue` or :keyword:`return` statement, the
Expand All @@ -430,7 +432,9 @@ points discuss more complex cases when an exception occurs:
statement, the returned value will be the one from the
:keyword:`!finally` clause's :keyword:`!return` statement, not the
value from the :keyword:`!try` clause's :keyword:`!return`
statement.
statement. This can be confusing and is therefore discouraged. From
version 3.14 the compiler emits a :exc:`SyntaxWarning` for it
(see :pep:`765`).

For example::

Expand Down
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Summary -- release highlights
* :ref:`PEP 741: Python Configuration C API <whatsnew314-pep741>`
* :ref:`PEP 761: Discontinuation of PGP signatures <whatsnew314-pep761>`
* :ref:`A new type of interpreter <whatsnew314-tail-call>`
* :ref:`PEP 765: Disallow return/break/continue that exit a finally block <whatsnew314-pep765>`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the reader, I think it may be better to have the PEP bullet points grouped.



Incompatible changes
Expand Down Expand Up @@ -370,6 +371,15 @@ Other language changes
The testbed can also be used to run the test suite of projects other than
CPython itself. (Contributed by Russell Keith-Magee in :gh:`127592`.)

.. _whatsnew314-pep765:

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

The compiler emits a :exc:`SyntaxWarning` when a :keyword:`return`, :keyword:`break` or
:keyword:`continue` statements appears where it exits a :keyword:`finally` block.
This change is specified in :pep:`765`.

New modules
===========

Expand Down
7 changes: 5 additions & 2 deletions Include/internal/pycore_compile.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ extern int _PyCompile_AstOptimize(
PyObject *filename,
PyCompilerFlags *flags,
int optimize,
struct _arena *arena);
struct _arena *arena,
int syntax_check_only);

extern int _PyAST_Optimize(
struct _mod *,
struct _arena *arena,
PyObject *filename,
int optimize,
int ff_features);
int ff_features,
int syntax_check_only);


typedef struct {
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test___all__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def check_all(self, modname):
(".* (module|package)", DeprecationWarning),
(".* (module|package)", PendingDeprecationWarning),
("", ResourceWarning),
("", SyntaxWarning),
quiet=True):
try:
exec("import %s" % modname, names)
Expand All @@ -52,6 +53,7 @@ def check_all(self, modname):
with warnings_helper.check_warnings(
("", DeprecationWarning),
("", ResourceWarning),
("", SyntaxWarning),
quiet=True):
try:
exec("from %s import *" % modname, names)
Expand Down
55 changes: 55 additions & 0 deletions Lib/test/test_ast/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,61 @@ def test_repr_large_input_crash(self):
r"Exceeds the limit \(\d+ digits\)"):
repr(ast.Constant(value=eval(source)))

def test_pep_765_warnings(self):
srcs = [
textwrap.dedent("""
def f():
try:
pass
finally:
return 42
"""),
textwrap.dedent("""
for x in y:
try:
pass
finally:
break
"""),
textwrap.dedent("""
for x in y:
try:
pass
finally:
continue
"""),
]
for src in srcs:
with self.assertWarnsRegex(SyntaxWarning, 'finally'):
ast.parse(src)

def test_pep_765_no_warnings(self):
srcs = [
textwrap.dedent("""
try:
pass
finally:
def f():
return 42
"""),
textwrap.dedent("""
try:
pass
finally:
for x in y:
break
"""),
textwrap.dedent("""
try:
pass
finally:
for x in y:
continue
"""),
]
for src in srcs:
ast.parse(src)


class CopyTests(unittest.TestCase):
"""Test copying and pickling AST nodes."""
Expand Down
6 changes: 4 additions & 2 deletions Lib/test/test_except_star.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ def test_break_in_except_star(self):
if i == 2:
break
finally:
return 0
pass
return 0
""")


Expand Down Expand Up @@ -117,7 +118,8 @@ def test_continue_in_except_star_block_invalid(self):
if i == 2:
continue
finally:
return 0
pass
return 0
""")

def test_return_in_except_star_block_invalid(self):
Expand Down
Loading
Loading