Skip to content

Commit b2f8126

Browse files
committed
PEP 765: add report as appendix
1 parent a159185 commit b2f8126

File tree

1 file changed

+232
-1
lines changed

1 file changed

+232
-1
lines changed

peps/pep-0765.rst

Lines changed: 232 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Rationale
5454
=========
5555

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

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

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

@@ -154,6 +156,235 @@ that their code needs to change. The `empirical evidence
154156
shows that the changes necessary are typically quite
155157
straightforward.
156158

159+
Appendix
160+
========
161+
162+
Below is an abridged version of a
163+
`research report<https://github.com/iritkatriel/finally/commits/main/README.md>`__
164+
by Irit Katriel, which was posted on 9 Nov 2024.
165+
166+
``return`` in ``finally`` considered harmful
167+
--------------------------------------------
168+
169+
I describe an investigation of usage of ``return``, ``break`` and ``continue``
170+
in a ``finally`` clause in real world code. It addresses the
171+
questions: Are people using it? How often are they using it incorrectly?
172+
How much churn would the proposed change create?
173+
174+
Method
175+
^^^^^^
176+
177+
The analysis is based on the 8,000 most popular PyPI packages, in terms of number
178+
of downloads in the last 30 days. They were downloaded on the 17th-18th of
179+
October, using
180+
`a script<https://github.com/faster-cpython/tools/blob/main/scripts/download_packages.py>`__
181+
written by Guido van Rossum, which in turn relies on Hugo van Kemenade's
182+
`tool<https://hugovk.github.io/top-pypi-packages/>`__ that creates a list of the
183+
most popular packages.
184+
185+
Once downloaded, a
186+
`second script<https://github.com/iritkatriel/finally/blob/main/scripts/ast_analysis.py>`__
187+
was used to construct an AST for each file, and traverse it to identify ``break``,
188+
``continue`` and ``return`` statements which are directly inside a ``finally`` block.
189+
190+
I then found the current source code for each occurence, and categorized it. For
191+
cases where the code seems incorrect, I created an issue in the project's bug
192+
tracker. The responses to these issues are also part of the data collected in
193+
this investigation.
194+
195+
Results
196+
^^^^^^^
197+
198+
I decided not to include a list of the incorrect usages, out of concern that
199+
it would make this report look like a shaming exercise. Instead I will describe
200+
the results in general terms, but will mention that some of the problems I found
201+
appear in very popular libraries, including a cloud security application.
202+
For those so inclined, it should not be hard to replicate my analysis, as I
203+
provided links to the scripts I used in the Method section.
204+
205+
The projects examined contained a total of 120,964,221 lines of Python code,
206+
and among them the script found 203 instances of control flow instructions in a
207+
``finally`` block. Most were ``return``, a handful were ``break``, and none were
208+
``continue``. Of these:
209+
210+
- 46 are correct, and appear in tests that target this pattern as a feature (e.g.,
211+
tests for linters that detect it).
212+
- 8 seem like they could be correct - either intentionally swallowing exceptions
213+
or appearing where an active exception cannot occur. Despite being correct, it is
214+
not hard to rewrite them to avoid the bad pattern, and it would make the code
215+
clearer: deliberately swallowing exceptions can be more explicitly done with
216+
``except BaseException:``, and ``return`` which doesn't swallow exceptions can be
217+
moved after the ``finally`` block.
218+
- 149 were clearly incorrect, and can lead to unintended swallowing of exceptions.
219+
These are analyzed in the next section.
220+
221+
**The Error Cases**
222+
223+
Many of the error cases followed this pattern:
224+
225+
.. code-block::
226+
:class: bad
227+
228+
try:
229+
...
230+
except SomeSpecificError:
231+
...
232+
except Exception:
233+
logger.log(...)
234+
finally:
235+
return some_value
236+
237+
Code like this is obviously incorrect because it deliberately logs and swallows
238+
``Exception`` subclasses, while silently swallowing ``BaseExceptions``. The intention
239+
here is either to allow ``BaseExceptions`` to propagate on, or (if the author is
240+
unaware of the ``BaseException`` issue), to log and swallow all exceptions. However,
241+
even if the ``except Exception`` was changed to ``except BaseException``, this code
242+
would still have the problem that the ``finally`` block swallows all exceptions
243+
raised from within the ``except`` block, and this is probably not the intention
244+
(if it is, that can be made explicit with another ``try``-``except BaseException``).
245+
246+
Another variation on the issue found in real code looks like this:
247+
248+
.. code-block::
249+
:class: bad
250+
251+
try:
252+
...
253+
except:
254+
return NotImplemented
255+
finally:
256+
return some_value
257+
258+
Here the intention seems to be to return ``NotImplemented`` when an exception is
259+
raised, but the ``return`` in the ``finally`` block would override the one in the
260+
``except`` block.
261+
262+
> [!NOTE]
263+
> Following the
264+
> `discussion<https://discuss.python.org/t/an-analysis-of-return-in-finally-in-the-wild/70633/15>`__,
265+
> I repeated the analysis on a random selection of PyPI packages (to
266+
> analyze code written by *average* programmers). The sample contained
267+
> in total 77,398,892 lines of code with 316 instances of ``return``/``break``/``continue``
268+
> in ``finally``. So about 4 instances per million lines of code.
269+
270+
**Author reactions**
271+
272+
Of the 149 incorrect instances of ``return`` or ``break`` in a ``finally`` clause,
273+
27 were out of date, in the sense that they do not appear in the main/master branch
274+
of the library, as the code has been deleted or fixed by now. The remaining 122
275+
are in 73 different packages, and I created an issue in each one to alert the
276+
authors to the problems. Within two weeks, 40 of the 73 issues received a reaction
277+
from the code maintainers:
278+
279+
- 15 issues had a PR opened to fix the problem.
280+
- 20 received reactions acknowleding the problem as one worth looking into.
281+
- 3 replied that the code is no longer maintained so this won't be fixed.
282+
- 2 closed the issue as "works as intended", one said that they intend to
283+
swallow all exceptions, but the other seemed unaware of the distinction
284+
between ``Exception`` and ``BaseException``.
285+
286+
One issue was linked to a pre-existing open issue about non-responsiveness to Ctrl-C,
287+
conjecturing a connection.
288+
289+
Two of the issue were labelled as "good first issue".
290+
291+
**The correct usages**
292+
293+
The 8 cases where the feature appears to be used correctly (in non-test code) also
294+
deserve attention. These represent the "churn" that would be caused by blocking
295+
the feature, because this is where working code will need to change. I did not
296+
contact the authors in these cases, so we will need to assess the difficulty of
297+
making these changes ourselves.
298+
299+
- In `mosaicml<https://github.com/mosaicml/composer/blob/694e72159cf026b838ba00333ddf413185b4fb4f/composer/cli/launcher.py#L590>`__
300+
there is a return in a finally at the end of the ``main`` function, after an ``except:``
301+
clause which swallows all exceptions. The return in the finally would swallow
302+
an exception raised from within the ``except:`` clause, but this seems to be the
303+
intention in this code. A possible fix would be to assign the return value to a
304+
variable in the ``finally`` clause, dedent the ``return`` statement and wrap the
305+
body of the ``except:`` clause by another ``try``-``except`` to swallow any
306+
exceptions from within it.
307+
308+
- In `webtest<https://github.com/Pylons/webtest/blob/617a2b823c60e8d7c5f6e12a220affbc72e09d7d/webtest/http.py#L131>`__
309+
there is a ``finally`` block that contains only ``return False``. It could be replaced
310+
by
311+
312+
.. code-block::
313+
314+
except BaseException:
315+
pass
316+
return False
317+
318+
- In `kivy<https://github.com/kivy/kivy/blob/3b744c7ed274c1a99bd013fc55a5191ebd8a6a40/kivy/uix/codeinput.py#L204>`__
319+
there is a ``finally`` that contains only a ``return`` statement. Since there is also a
320+
bare ``except`` just before it, in this case the fix will be to just remove the ``finally:``
321+
block and dedent the ``return`` statement.
322+
323+
- In `logilab-common<https://forge.extranet.logilab.fr/open-source/logilab-common/-/blob/branch/default/test/data/module.py?ref_type=heads#L60>`__
324+
there is, once again, a ``finally`` clause that can be replace by an ``except BaseException``
325+
with the ``return`` dedented one level.
326+
327+
- In `pluggy<https://github.com/pytest-dev/pluggy/blob/c760a77e17d512c3572d54d368fe6c6f9a7ac810/src/pluggy/_callers.py#L141>`__
328+
there is a lengthy ``finally`` with two ``return`` statements (the second on line 182). Here the
329+
return value can be assigned to a variable, and the ``return`` itself can appear after we've
330+
exited the ``finally`` clause.
331+
332+
- In `aws-sam-cli<https://github.com/aws/aws-sam-cli/blob/97e63dcc2738529eded8eecfef4b875abc3a476f/samcli/local/apigw/local_apigw_service.py#L721>`__
333+
there is a conditional return at the end of the block.
334+
From reading the code, it seems that the condition only holds when the exception has
335+
been handled. The conditional block can just move outside of the ``finally`` block and
336+
achieve the same effect.
337+
338+
- In `scrappy<https://github.com/scrapy/scrapy/blob/52c072640aa61884de05214cb1bdda07c2a87bef/scrapy/utils/gz.py#L27>`__
339+
there is a ``finally`` that contains only a ``break`` instruction. Assuming that it was the intention
340+
to swallow all exceptions, it can be replaced by
341+
342+
.. code-block::
343+
344+
except BaseException:
345+
pass
346+
break
347+
348+
Discussion
349+
^^^^^^^^^^
350+
351+
The first thing to note is that ``return``/``break``/``continue`` in a ``finally``
352+
block is not something we see often: 203 instance in over 120 million lines
353+
of code. This is, possibly, thanks to the linters that warn about this.
354+
355+
The second observation is that most of the usages were incorrect: 73% in our
356+
sample (149 of 203).
357+
358+
Finally, the author responses were overwhelmingly positive. Of the 40 responses
359+
received within two weeks, 35 acknowledged the issue, 15 of which also created
360+
a PR to fix it. Only two thought that the code is fine as it is, and three
361+
stated that the code is no longer maintained so they will not look into it.
362+
363+
The 8 instances where the code seems to work as intended, are not hard to
364+
rewrite.
365+
366+
Conclusion
367+
^^^^^^^^^^
368+
369+
The results indicate that ``return``, ``break`` and ``continue`` in a finally block
370+
371+
- are rarely used.
372+
- when they are used, they are usually used incorrectly.
373+
- code authors are receptive to changing their code, and tend to find it easy to do.
374+
375+
## Acknowledgements
376+
377+
I thank:
378+
379+
- Alyssa Coghlan for
380+
`bringing this issue my attention<https://discuss.python.org/t/pep-760-no-more-bare-excepts/67182/97>`__.
381+
382+
- Guido van Rossum and Hugo van Kemenade for the
383+
`script<https://github.com/faster-cpython/tools/blob/main/scripts/download_packages.py>`__
384+
that downloads the most popular PyPI packages.
385+
386+
- The many code authors I contacted for their responsiveness and grace.
387+
157388
Copyright
158389
=========
159390

0 commit comments

Comments
 (0)