diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fa38b768e..449127b2a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -641,6 +641,7 @@ peps/pep-0760.rst @pablogsal @brettcannon peps/pep-0761.rst @sethmlarson @hugovk peps/pep-0762.rst @pablogsal @ambv @lysnikolaou @emilyemorehouse peps/pep-0763.rst @dstufft +peps/pep-0765.rst @iritkatriel @ncoghlan # ... peps/pep-0777.rst @warsaw # ... diff --git a/peps/pep-0765.rst b/peps/pep-0765.rst new file mode 100644 index 000000000..104a1b821 --- /dev/null +++ b/peps/pep-0765.rst @@ -0,0 +1,160 @@ +PEP: 765 +Title: Disallow return/break/continue that exit a finally block +Author: Irit Katriel , Alyssa Coghlan +Discussions-To: https://discuss.python.org/t/an-analysis-of-return-in-finally-in-the-wild/70633 +Status: Draft +Type: Standards Track +Created: 15-Nov-2024 +Python-Version: 3.14 +Post-History: `09-Nov-2024 `__, + +Abstract +======== + +This PEP proposes to withdraw support for ``return``, ``break`` and +``continue`` statements that break out of a ``finally`` block. +This was proposed in the past by :pep:`601`. The current PEP +is based on empirical evidence regarding the cost/benefit of +this change, which did not exist at the time that :pep:`601` +was rejected. It also proposes a slightly different solution +than that which was proposed by :pep:`601`. + +Motivation +========== + +The semantics of ``return``, ``break`` and ``continue`` in a +finally block are surprising for many developers. +The :ref:`documentation ` mentions that: + +- If the ``finally`` clause executes a ``break``, ``continue`` + or ``return`` statement, exceptions are not re-raised. + +- If a ``finally`` clause includes a ``return`` statement, the + returned value will be the one from the ``finally`` clause’s + ``return`` statement, not the value from the ``try`` clause’s + ``return`` statement. + +Both of these behaviours cause confusion, but the first is +particularly dangerous because a swallowed exception is more +likely to slip through testing, than an incorrect return value. + +In 2019, :pep:`601` proposed to change Python to emit a +``SyntaxWarning`` for a few releases and then turn it into a +``SyntaxError``. It was rejected in favour of viewing this +as a programming style issue, to be handled by linters and :pep:`8`. +Indeed, :pep:`8` now recommends not to use control flow statements +in a ``finally`` block, and linters such as +`Pylint `__, +`Ruff `__ and +`flake8-bugbear `__ +flag them as a problem. + +Rationale +========= + +A recent `analysis of real world code +`_ shows that: + +- These features are rare (2 per million LOC in the top 8,000 PyPI + packages, 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 unintended + exception-swallowing bugs. +- Code owners are typically receptive to fixing the bugs, and + find that easy to do. + +This new data indicates that it would benefit Python's users if +Python itself moved them away from this harmful feature. + +`One of the arguments brought up +`__ +was that language features should be orthogonal, and combine without +context-based restrictions. However, in the meantime :pep:`654` has +been implemented, and it forbids ``return``, ``break`` and ``continue`` +in an ``except*`` clause because the semantics of that would violate +the property that ``except*`` clauses operate *in parallel*, so the +code of one clause should not suppress the invocation of another. +In that case we accepted that a combination of features can be +harmful enough that it makes sense to disallow it. + + +Specification +============= + +The change is to specify as part of the language spec that +Python's compiler may emit a ``SyntaxWarning`` or ``SyntaxError`` +when a ``return``, ``break`` or ``continue`` would transfer +control flow from within a ``finally`` block to a location outside +of it. + +This includes the following examples: + +.. code-block:: + :class: bad + + def f(): + try: + ... + finally: + return 42 + + for x in o: + try: + ... + finally: + break # (or continue) + +But excludes these: + +.. code-block:: + :class: good + + try: + ... + finally: + def f(): + return 42 + + try: + ... + finally: + for x in o: + break # (or continue) + + +CPython will emit a ``SyntaxWarning`` in version 3.14, and we leave +it open whether, and when, this will become a ``SyntaxError``. +However, we specify here that a ``SyntaxError`` is permitted by +the language spec, so that other Python implementations can choose +to implement that. + +Backwards Compatibility +======================= + +For backwards compatibility reasons, we are proposing that CPython +emit only a ``SyntaxWarning``, with no concrete plan to upgrade that +to an error. Code running with ``-We`` may stop working once this +is 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 issues +related to raising a new ``SyntaxWarning`` or ``SyntaxError``. + +How to Teach This +================= + +The change will be documented in the language spec and in the +What's New documentation. The ``SyntaxWarning`` will alert users +that their code needs to change. The `empirical evidence +`__ +shows that the changes necessary are typically quite +straightforward. + +Copyright +========= + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive.