@@ -54,7 +54,7 @@ Rationale
5454=========
5555
5656A 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+
6769This new data indicates that it would benefit Python's users if
6870Python itself moved them away from this harmful feature.
6971
@@ -154,6 +156,235 @@ that their code needs to change. The `empirical evidence
154156shows that the changes necessary are typically quite
155157straightforward.
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+
157388Copyright
158389=========
159390
0 commit comments