Skip to content

Commit 6083e27

Browse files
authored
PEP 765: add report as appendix (#4125)
1 parent 798ee6e commit 6083e27

File tree

1 file changed

+165
-6
lines changed

1 file changed

+165
-6
lines changed

peps/pep-0765.rst

Lines changed: 165 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ flag them as a problem.
5454
Rationale
5555
=========
5656

57-
A recent `analysis of real world code
58-
<https://github.com/iritkatriel/finally/blob/main/README.md>`_ shows that:
57+
A recent
58+
`analysis of real world code <https://github.com/iritkatriel/finally/blob/main/README.md>`__ shows that:
5959

6060
- These features are rare (2 per million LOC in the top 8,000 PyPI
6161
packages, 4 per million LOC in a random selection of packages).
@@ -65,11 +65,13 @@ A recent `analysis of real world code
6565
- Code owners are typically receptive to fixing the bugs, and
6666
find that easy to do.
6767

68+
See `the appendix <#appendix>`__ for more details.
69+
6870
This new data indicates that it would benefit Python's users if
6971
Python itself moved them away from this harmful feature.
7072

71-
One of the arguments brought up in `the PEP 601 discussion
72-
<https://discuss.python.org/t/pep-601-forbid-return-break-continue-breaking-out-of-finally/2239/24>`__
73+
One of the arguments brought up in
74+
`the PEP 601 discussion <https://discuss.python.org/t/pep-601-forbid-return-break-continue-breaking-out-of-finally/2239/24>`__
7375
was that language features should be orthogonal, and combine without
7476
context-based restrictions. However, in the meantime :pep:`654` has
7577
been implemented, and it forbids ``return``, ``break`` and ``continue``
@@ -150,11 +152,168 @@ How to Teach This
150152

151153
The change will be documented in the language spec and in the
152154
What's New documentation. The ``SyntaxWarning`` will alert users
153-
that their code needs to change. The `empirical evidence
154-
<https://github.com/iritkatriel/finally/blob/main/README.md>`__
155+
that their code needs to change. The `empirical evidence <#appendix>`__
155156
shows that the changes necessary are typically quite
156157
straightforward.
157158

159+
Appendix
160+
========
161+
162+
``return`` in ``finally`` considered harmful
163+
--------------------------------------------
164+
165+
Below is an abridged version of a
166+
`research report <https://github.com/iritkatriel/finally/commits/main/README.md>`__
167+
by Irit Katriel, which was posted on 9 Nov 2024.
168+
It describes an investigation into usage of ``return``, ``break`` and ``continue``
169+
in a ``finally`` clause in real world code, addressing the
170+
questions: Are people using it? How often are they using it incorrectly?
171+
How much churn would the proposed change create?
172+
173+
Method
174+
^^^^^^
175+
176+
The analysis is based on the 8,000 most popular PyPI packages, in terms of number
177+
of downloads in the last 30 days. They were downloaded on the 17th-18th of
178+
October, using
179+
`a script <https://github.com/faster-cpython/tools/blob/main/scripts/download_packages.py>`__
180+
written by Guido van Rossum, which in turn relies on Hugo van Kemenade's
181+
`tool <https://hugovk.github.io/top-pypi-packages/>`__ that creates a list of the
182+
most popular packages.
183+
184+
Once downloaded, a
185+
`second script <https://github.com/iritkatriel/finally/blob/main/scripts/ast_analysis.py>`__
186+
was used to construct an AST for each file, and traverse it to identify ``break``,
187+
``continue`` and ``return`` statements which are directly inside a ``finally`` block.
188+
189+
I then found the current source code for each occurrence, and categorized it. For
190+
cases where the code seems incorrect, I created an issue in the project's bug
191+
tracker. The responses to these issues are also part of the data collected in
192+
this investigation.
193+
194+
Results
195+
^^^^^^^
196+
197+
I decided not to include a list of the incorrect usages, out of concern that
198+
it would make this report look like a shaming exercise. Instead I will describe
199+
the results in general terms, but will mention that some of the problems I found
200+
appear in very popular libraries, including a cloud security application.
201+
For those so inclined, it should not be hard to replicate my analysis, as I
202+
provided links to the scripts I used in the Method section.
203+
204+
The projects examined contained a total of 120,964,221 lines of Python code,
205+
and among them the script found 203 instances of control flow instructions in a
206+
``finally`` block. Most were ``return``, a handful were ``break``, and none were
207+
``continue``. Of these:
208+
209+
- 46 are correct, and appear in tests that target this pattern as a feature (e.g.,
210+
tests for linters that detect it).
211+
- 8 seem like they could be correct - either intentionally swallowing exceptions
212+
or appearing where an active exception cannot occur. Despite being correct, it is
213+
not hard to rewrite them to avoid the bad pattern, and it would make the code
214+
clearer: deliberately swallowing exceptions can be more explicitly done with
215+
``except BaseException:``, and ``return`` which doesn't swallow exceptions can be
216+
moved after the ``finally`` block.
217+
- 149 were clearly incorrect, and can lead to unintended swallowing of exceptions.
218+
These are analyzed in the next section.
219+
220+
**The Error Cases**
221+
222+
Many of the error cases followed this pattern:
223+
224+
.. code-block::
225+
:class: bad
226+
227+
try:
228+
...
229+
except SomeSpecificError:
230+
...
231+
except Exception:
232+
logger.log(...)
233+
finally:
234+
return some_value
235+
236+
Code like this is obviously incorrect because it deliberately logs and swallows
237+
``Exception`` subclasses, while silently swallowing ``BaseExceptions``. The intention
238+
here is either to allow ``BaseExceptions`` to propagate on, or (if the author is
239+
unaware of the ``BaseException`` issue), to log and swallow all exceptions. However,
240+
even if the ``except Exception`` was changed to ``except BaseException``, this code
241+
would still have the problem that the ``finally`` block swallows all exceptions
242+
raised from within the ``except`` block, and this is probably not the intention
243+
(if it is, that can be made explicit with another ``try``-``except BaseException``).
244+
245+
Another variation on the issue found in real code looks like this:
246+
247+
.. code-block::
248+
:class: bad
249+
250+
try:
251+
...
252+
except:
253+
return NotImplemented
254+
finally:
255+
return some_value
256+
257+
Here the intention seems to be to return ``NotImplemented`` when an exception is
258+
raised, but the ``return`` in the ``finally`` block would override the one in the
259+
``except`` block.
260+
261+
.. note:: Following the
262+
`discussion <https://discuss.python.org/t/an-analysis-of-return-in-finally-in-the-wild/70633/15>`__,
263+
I repeated the analysis on a random selection of PyPI packages (to
264+
analyze code written by *average* programmers). The sample contained
265+
in total 77,398,892 lines of code with 316 instances of ``return``/``break``/``continue``
266+
in ``finally``. So about 4 instances per million lines of code.
267+
268+
**Author reactions**
269+
270+
Of the 149 incorrect instances of ``return`` or ``break`` in a ``finally`` clause,
271+
27 were out of date, in the sense that they do not appear in the main/master branch
272+
of the library, as the code has been deleted or fixed by now. The remaining 122
273+
are in 73 different packages, and I created an issue in each one to alert the
274+
authors to the problems. Within two weeks, 40 of the 73 issues received a reaction
275+
from the code maintainers:
276+
277+
- 15 issues had a PR opened to fix the problem.
278+
- 20 received reactions acknowledging the problem as one worth looking into.
279+
- 3 replied that the code is no longer maintained so this won't be fixed.
280+
- 2 closed the issue as "works as intended", one said that they intend to
281+
swallow all exceptions, but the other seemed unaware of the distinction
282+
between ``Exception`` and ``BaseException``.
283+
284+
One issue was linked to a pre-existing open issue about non-responsiveness to Ctrl-C,
285+
conjecturing a connection.
286+
287+
Two of the issue were labelled as "good first issue".
288+
289+
**The correct usages**
290+
291+
The 8 cases where the feature appears to be used correctly (in non-test code) also
292+
deserve attention. These represent the "churn" that would be caused by blocking
293+
the feature, because this is where working code will need to change. I did not
294+
contact the authors in these cases, so we need to assess the difficulty of
295+
making these changes ourselves. It is shown in
296+
`the full report <https://github.com/iritkatriel/finally/commits/main/README.md>`__,
297+
that the change required in each case is small.
298+
299+
Discussion
300+
^^^^^^^^^^
301+
302+
The first thing to note is that ``return``/``break``/``continue`` in a ``finally``
303+
block is not something we see often: 203 instance in over 120 million lines
304+
of code. This is, possibly, thanks to the linters that warn about this.
305+
306+
The second observation is that most of the usages were incorrect: 73% in our
307+
sample (149 of 203).
308+
309+
Finally, the author responses were overwhelmingly positive. Of the 40 responses
310+
received within two weeks, 35 acknowledged the issue, 15 of which also created
311+
a PR to fix it. Only two thought that the code is fine as it is, and three
312+
stated that the code is no longer maintained so they will not look into it.
313+
314+
The 8 instances where the code seems to work as intended, are not hard to
315+
rewrite.
316+
158317
Copyright
159318
=========
160319

0 commit comments

Comments
 (0)