So the return/break/continue statement in that finally block would be silently ignored?
You may argue that this is the “correct” behavior, but that doesn’t seem obvious to me. The problem is we don’t know what the original developer intended. Python’s usual stance on this is to “avoid the temptation to guess”.
Only in that it doesn’t exit the function (which is already exiting due to an exception) or repeat the loop (which is already broken due to an exception).
Any later statements in the finally block would not be executed. And any outer finally blocks would still be executed (thanks Guido!).
Worth noting that raising an exception from a finally block when an exception is already being raised will chain the exception, which means we already acknowledge that the original exception is still relevant. Applying that logic to all ways to early-exit the finally block seems pretty consistent to me (though making that change is nearly as hard as making the proposed one, hence my preference for status quo).
I don’t like this change. Either make it an error or don’t make it a warning. This halfway-inbetween stance is targeting a small number of users and telling them they are wrong, but without the conviction to actually make it an error, and instead doing it in a way that turns it into “lets annoy your users to shame you into changing it”
For what it’s worth, I don’t think this should be an error or a warning, and this would make some code I maintain much more convoluted where finally is intentionally used to silence errors and return a value, and there is exception handling involved, requiring code duplication in branches to actually get the desired ordering.
I don’t see how that combination of desiderata is achievable. The only way to remove it is to break stuff; if you raise a warning you don’t break stuff but also don’t remove it.
Regardless of “should”, I don’t see how there can be a way.
This is something I’ve seen in other discussions about backwards compatibility, and I think it kind of misses the point. In my view, the important inflection point in terms of backwards compatibility is between “this code can still run with zero changes” and “this code can’t run without changes”. It doesn’t matter whether code that gets broken by a change is “easy to rewrite”; the point of backwards compatibility is to ensure that old code can run exactly as is.
As I mentioned in an earlier post, it might sometimes be worthwhile to break backwards compatibility, but I don’t see it as useful to try to break it “just a little bit” to fix minor things like this.
So if we don’t want to break things, we can’t change this behavior. If we can’t change the behavior, I don’t think it’s a great idea to just add a warning. There are endless dubious coding practices we could warn about, but I’d rather not go down that slippery slope. It would be better to encourage linters to add a check for this, and perhaps to add a warning note to the docs.
The proposal moves the construct into the “may not work on all implementations” category. Allowing new Python implementations to skip implementing this feature was the primary motivation in PEP 601 (the MicroPython devs didn’t want to support it), and that aspect is still part of the motivation for this PEP.
Essentially, we want to demote this behaviour from “Python language feature that all compliant implementations offer” to “CPython implementation detail that other implementations may or may not support”. That can be done without adding even the syntax warning to CPython, but if the demotion happens, a syntax warning makes the portability issue explicit.
I don’t know if this is the best place to chip in specific feedback on wording in the PEP, since the discussion is still fairly high-level. But in reading the PEP for the first time, just one small clarity thing.
In the Specifications section, it’s not super clear at first glance which examples have new behavior and which don’t, just based on the verbs “include”/“exclude”. Perhaps the headings could change to:
“This includes the following examples” → "These examples may emit a SyntaxWarning or SyntaxError
and
“But excludes these” → “These examples would not emit the warning or error”
I’ve been mulling this proposal over for a few days now since first noticing this thread.
Personally I have been bitten by this gotcha many times, and IMO being able to exit a finally block in this way should never have been allowed in the first place.
Although I don’t like the idea of adding a SyntaxWarning for something without a plan to eventually make it a SyntaxError - it doesn’t feel like the right way to do things - I suppose it still brings the benefit of being alerted to the problem, and doesn’t really have a downside in practice, only in philosophy. So I guess I can say I’m -0 on this PEP.
What I’d love to see is for PEP 601, which would have made this a SyntaxError, to be revived, reconsidered, and accepted. I suppose if PEP 765 gets accepted, and that causes PEP 601 to become acceptable in future, then it will have been worth it.
Linters and static analysis tools should definitely gain a rule to forbid this facility if they don’t already have one, even if it’s optional and off by default; at least people could then opt in to forbidding it from their own code.
Forbidding it in your own code doesn’t help you if it’s one of your dependencies that is eating the exception. All linter based mitigations for this have that problem: they don’t tell you if one of your dependencies is at risk of making exceptions unexpectedly vanish.
By contrast, if the compiler is doing it, then app developers get notified no matter if it’s their own code or code in one of their dependencies which has the dubious construct.
I’m thoroughly sympathetic to folks favouring eventual escalation to a full SyntaxError (I was the core dev sponsor for PEP 601), but the following numbers from the project author responses in Irit’s report concern me:
“3 replied that the code is no longer maintained so this won’t be fixed.”
33 did not respond (at least in the time frame covered by the report)
When CPython is only emitting SyntaxWarning, app developers have options for attempting to suppress that warning (this can be done programmatically for implicit runtime compilation, but can be a bit trickier for installation time pre-compilation).
If CPython escalates to a full SyntaxError, the problem becomes much harder to work around without outright forking the affected dependency.
The SC discussed this PEP today and we’re happy to say that we accept it unanimously. The PEP was well-written and obviously persuasive. We were particularly swayed by the consistency with except*.
I’m not sure what I should recommend to this user other than “now you need to suppress specific warnings in your CI at a level above python”; It also feels wrong to change code that is working as intended because of what amounts to a linter warning with no specific plan to actually make it an error.
Yeah, I’m genuinely unsure what I should recommend. This is a user downstream from the library where the code exists.
The standard way to fix it without a code change of stable library code would be a warning filter provided by the library for this with us assuming responsibility for keeping aware of the situation and any later decision to promote this to an error, or suggest the user could do so in CI.
If the only answer that reasonably works on the library side is for the library to change the working code, this is effectively just as bad as a breaking change without standard notice, just using the fact that software is interconnected and the choice of where this is surfaced to break it by pressuring the social contract rather than it actually breaking the code that is running.
That was the main decision the Steering Council had to make regarding the PEP: whether inadvertently suppressing all exceptions was a frequent enough error to justify requiring folks that were intentionally suppressing all exceptions to tweak their code to make that intent clearer to readers.
For downstream consumers of affected libraries, ensuring their dependencies are precompiled at installation time should be sufficient to avoid the warning in CI (which pip does by default, but uv doesn’t). The warning is emitted during AST traversal, so imports from already compiled modules won’t say anything:
Okay, up above, one of the things people asked about was if downstream libraries would be affected, and we were told they would not because of precompilation. This seems to have been only accurate regarding pip. Perhaps the intent was unclear with this, or maybe there was an assumption that installers precompile unless told not to (which is not the case) impacting how this was conveyed, but the user effected has not explicitly skipped this:
Which because this is causing downstream CI to error, has basically become a breaking change,
Any time we add a new DeprecationWarning is frequently a breaking change for CI systems, and SyntaxWarning isn’t any different in that regard.
The PEP specifically called this out in the Backwards Compatibility section: “Code running with -We may stop working once this is introduced.”
“Zero breaking changes” isn’t a design goal for feature releases though, it’s instead more:
any breaking changes should be justified; and
any breaking changes should preferably have a way to deal with them that’s also valid in older still supported releases (rather than users having to rely on explicit version checks)
The justification in this case was the 3:1 ratio found in public code between incorrect latent defects and correct usage of the feature (with only small updates needed in the latter cases to avoid relying on the dubious behaviour).
PEP 765 has a few approaches in the latter list:
avoid the construct the generates the warning
add an explicit warning filter to suppress the warning
precompile the code so the warning is emitted at a point where warnings aren’t reported as an error
I understand this, but when people were asking about if the impact might be too large, you reassured them that unless a user opts out of precompilation, it wouldn’t be visible downstream. This isn’t actually accurate, but I took it at face value, and seemingly so did others in the conversation. It happens to be accurate for pip, because currently anyway, that’s pip’s default behavior.
Fair. I don’t actually know what the level of adoption of uv is (aside from “higher than it was a year ago”), but there was certainly an assumption on my part at the time that the intersection of “uses uv”, “hasn’t turned precompilation back on” and “has a dependency affected by this issue” would be pretty close to the null set.
I’ll go ahead and pass on the option to precompile to the downstream user. I think there will be another release before 3.14 leaves rc status.
Sorry if some of this came across overly bitter, I realized when I came back to it that could be the case, I mostly just want to make sure that when impacts are considered, that we’re keeping in mind various things like pip not being the only resolver, and that defaults for others have to be checked as well.
I don’t think in the grand scheme of things this one is a big deal, but as a process thing, I wouldn’t want the same happening on a larger change.