@@ -54,8 +54,8 @@ flag them as a problem.
5454Rationale
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+
6870This new data indicates that it would benefit Python's users if
6971Python 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 >`__
7375was that language features should be orthogonal, and combine without
7476context-based restrictions. However, in the meantime :pep: `654 ` has
7577been implemented, and it forbids ``return ``, ``break `` and ``continue ``
@@ -150,11 +152,168 @@ How to Teach This
150152
151153The change will be documented in the language spec and in the
152154What'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 >`__
155156shows that the changes necessary are typically quite
156157straightforward.
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+
158317Copyright
159318=========
160319
0 commit comments