> The projects examined contained a total of 120,964,221 lines of Python code, and among them the script found 203 instances of control flow instructions in a finally block. Most were return, a handful were break, and none were continue.
I don't really write a lot of Python, but I do write a lot of Java, and `continue` is the main control flow statement that makes sense to me within a finally block.
I think it makes sense when implementing a generic transaction loop, something along the lines of:
<T> T executeTransaction(Function<Transaction, T> fn) {
for (int tries = 0;; tries++) {
var tx = newTransaction();
try {
return fn.apply(tx);
} finally {
if (!tx.commit()) {
// TODO: potentially log number of tries, maybe include a backoff, maybe fail after a certain number
continue;
}
}
}
}
In these cases "swallowing" the exception is often intentional, since the exception could be due to some logic failing as a result of inconsistent reads, so the transaction should be retried.
The alternative ways of writing this seem more awkward to me. Either you need to store the result (returned value or thrown exception) in one or two variables, or you need to duplicate the condition and the `continue;` behaviour. Having the retry logic within the `finally` block seems like the best way of denoting the intention to me, since the intention is to swallow the result, whether that was a return or a throw.
If there are particular exceptions that should not be retried, these would need to be caught/rethrown and a boolean set to disable the condition in the `finally` block, though to me this still seems easier to reason about than the alternatives.
That's not just Java and there is nothing really cursed about it: throwing in a finally block is the most common example. Jump statements are no different, you can't just ignore them when they override the return or throw statements.
If by coroutines the author meant virtual threads, then monitors have always been compatible with virtual threads (which have always needed to adhere to the Thread specification). Monitors could, for a short while, degrade the scalability of virtual threads (and in some situations even lead to deadlocks), but that has since been resolved in JDK 24 (https://openjdk.org/jeps/491).
I think it's coroutines as in other JVM languages like Kotlin, where yielding may be implemented internally as return (due to lack of native coroutine support in JVM).
Holding a lock/monitor across a yield is a bad idea for other reasons, so it shouldn't be a big deal in practice.
Doesn't JRE has some limited form of decompilation in its JIT, as a pre-pass? IIRC, it reconstructs the basic blocks and CFG from the bytecode and does some minor optimizations before going on to regalloc and codegen.
Older versions of Java did try to have only one copy of the finally block code. To implement this, there were "jsr" and "ret" instructions, which allowed a method (a subroutine) to contain subroutines inside it. This even curseder implementation of finally is prohibited starting from version 51 class files (Java 7).
On the subject
is my favorite cursed Java exceptions construct.To anyone wondering, I believe it's cursed because the finally continue blocks hijacks the try return, so the for loop never returns
Python has the same construct but is removing it, starting with a warning in version 3.14: https://peps.python.org/pep-0765/
Interesting .. from the post above:
> The projects examined contained a total of 120,964,221 lines of Python code, and among them the script found 203 instances of control flow instructions in a finally block. Most were return, a handful were break, and none were continue.
I don't really write a lot of Python, but I do write a lot of Java, and `continue` is the main control flow statement that makes sense to me within a finally block.
I think it makes sense when implementing a generic transaction loop, something along the lines of:
In these cases "swallowing" the exception is often intentional, since the exception could be due to some logic failing as a result of inconsistent reads, so the transaction should be retried.The alternative ways of writing this seem more awkward to me. Either you need to store the result (returned value or thrown exception) in one or two variables, or you need to duplicate the condition and the `continue;` behaviour. Having the retry logic within the `finally` block seems like the best way of denoting the intention to me, since the intention is to swallow the result, whether that was a return or a throw.
If there are particular exceptions that should not be retried, these would need to be caught/rethrown and a boolean set to disable the condition in the `finally` block, though to me this still seems easier to reason about than the alternatives.
There was a recent talk at PYCON UK about it, by one of the authors of the PEP in question: <https://www.youtube.com/watch?v=vrVXgeD2fts>
Just tested that in C# and it seems they made the smart decision to not allow shenanigans like that in a finally block:
CS0157 Control cannot leave the body of a finally clause
That's not just Java and there is nothing really cursed about it: throwing in a finally block is the most common example. Jump statements are no different, you can't just ignore them when they override the return or throw statements.
It is just Java as far as I can tell. Other languages with a finally don't allow for explicitly exiting the finally block.
And JavaScript .. And Python (though as sibling posts have mentioned it looks like they're intending to make a breaking change to remove it).
EDIT: actually, the PEP points out that they intend for it to only be a warning in CPython, to avoid the breaking change
It is Java as C# disallow this
> override the return
How is this not cursed
This is exceedingly nasty. Well Done!
Nice post!
A minor point:
> monitors are incompatible with coroutines
If by coroutines the author meant virtual threads, then monitors have always been compatible with virtual threads (which have always needed to adhere to the Thread specification). Monitors could, for a short while, degrade the scalability of virtual threads (and in some situations even lead to deadlocks), but that has since been resolved in JDK 24 (https://openjdk.org/jeps/491).
I think it's coroutines as in other JVM languages like Kotlin, where yielding may be implemented internally as return (due to lack of native coroutine support in JVM).
Holding a lock/monitor across a yield is a bad idea for other reasons, so it shouldn't be a big deal in practice.
Doesn't JRE has some limited form of decompilation in its JIT, as a pre-pass? IIRC, it reconstructs the basic blocks and CFG from the bytecode and does some minor optimizations before going on to regalloc and codegen.
It's hard to call it decompilation as opposed to just regular compilation though.
Older versions of Java did try to have only one copy of the finally block code. To implement this, there were "jsr" and "ret" instructions, which allowed a method (a subroutine) to contain subroutines inside it. This even curseder implementation of finally is prohibited starting from version 51 class files (Java 7).