|
|
Subscribe / Log in / New account

Corner cases and exception types

By Jake Edge
August 13, 2019

Some unanticipated corner cases with Python's new "walrus" operator—described in our Python 3.8 overview—have cropped up recently. The problematic uses of the operator will be turned into errors before the final release, but just what exception should be raised came into question. It seems that the exception specified in the PEP for the operator may not really be the best choice, as a recent discussion hashed out.

PEP 572 ("Assignment Expressions") describes the walrus operator (though not by that name, which came later). It allows making assignments as part of another statement, like an if or while statement—or in a list comprehension. It is this latter use where the corner cases recently arose. The following is how the walrus operator is meant to be used in lists or list comprehensions (from the PEP):

# Reuse a value that's expensive to compute
[y := f(x), y**2, y**3]

# Share a subexpression between a comprehension filter clause and its output
filtered_data = [y for x in data if (y := f(x)) is not None]
In those comprehensions, y is assigned once but used multiple times.

But a bug report from Nick Coghlan pointed out some oddities:

>>> [i := 10 for i in range(5)]
[10, 10, 10, 10, 10]
>>> i
10
Normally, you would not expect the iteration variable (i) to leak out of the comprehension, but here it is assigned with the walrus operator; it is confusing, but is arguably logical. Another example seems plainly wrong:
>>> [False and (i := 10) for i in range(5)]
[False, False, False, False, False]
>>> i
4
Because of the short-circuit evaluation of the boolean expression, the "i := 10" is never even executed. But it still causes the iteration variable to leak out of the comprehension. As Coghlan pointed out, a non-executing walrus assignment deeply nested in a comprehension leaks the iteration variable as well.

He has submitted a pull request that makes some changes to the PEP (including slipping a "walrus operator" reference in) to outlaw these specific instances and others like them. The original PEP had specified that problems found in the parsing of the walrus operator would raise a TargetScopeError exception, which would be a subclass of SyntaxError. But that was questioned by Barry A. Warsaw in the bug:

I know the PEP defines TargetScopeError as a subclass of SyntaxError, but it doesn't really explain why a subclass is necessary. I think seeing "TargetScopeError" will be a head scratcher. Why not just SyntaxError without introducing a new exception?

Guido van Rossum agreed that perhaps it would be better to simply raise SyntaxError, though he was concerned that it would require a full PEP review. Coghlan explained the reasoning:

I believe our main motivation for separating it out was the fact that even though TargetScopeError is a compile-time error, the affected code is syntactically fine - there are just issues with unambiguously inferring the intended read/write location for the affected target names. (Subclassing SyntaxError is then a pragmatic concession to the fact that "SyntaxError" also de facto means "CompilationError")

Searching for "Python TargetScopeError" will also get folks to relevant information far more quickly than searching for "Python SyntaxError" will.

The discussion soon moved to the python-dev mailing list at the behest of Van Rossum. Warsaw started the thread by posting a summary of the debate. In his view, any PEP change would be relatively minor, so it makes sense to nail that all down before the 3.8 release, which is slated for October. In the bug report, Serhiy Storchaka had mentioned the IndentationError and TabError subclasses of SyntaxError as possible reasons to continue with TargetScopeError, but those "feel different" to Warsaw because those names are self-explanatory.

Tim Peters, who co-authored the PEP with Van Rossum and Chris Angelico, didn't really care what the exception is called, but was not convinced that SyntaxError is any better, really:

Whereas SyntaxError would give no clue whatsoever, and nothing useful to search for. In contrast, a search for TargetScopeError would presumably find a precisely relevant explanation as the top hit (indeed, it already does today).

I don't care because it's unlikely an error most people will make at all - or, for those too clever for their own good, make more than once ;-)

Most who posted in the thread were either in favor of switching to SyntaxError or ambivalent about such a change, with Steven D'Aprano being the main exception (so to speak). He was concerned with the idea of emitting a syntax error for something that was not syntactically incorrect. "There's a problem with the *semantics* not the syntax." But Warsaw was not convinced that the distinction is useful: "What you wrote is disallowed, so you have to change your code (i.e. syntax) to avoid the error." He also noted that PEP 572 specifies TargetScopeError as a subclass of SyntaxError, so even under the current definition the distinction is not being made.

Eric V. Smith concurred with Warsaw; in particular, Smith noted that he could not see a reason that a programmer would catch and handle SyntaxError and TargetScopeError separately. He suggested (as did others in the thread) that the text emitted for that error make it clear to the user where the problem lies. For example, Kyle Stanley suggested something like:

SyntaxError: Invalid scope defined by walrus operator: 'i := 10 for i in range(5)'

In a long post, Coghlan agreed with the overall consensus that a simple SyntaxError is the right path forward for 3.8. He does think that there is value in a new exception (perhaps with a better name, such as AssignmentScopeError) that would cover more than just walrus operator errors. But that is something that can wait to be considered for 3.9.

With three steering council members in favor (Coghlan, Warsaw, and Van Rossum) and two of the three PEP authors in agreement (Peters switched to being in favor after Coghlan's message), Van Rossum said that TargetScopeError should be removed. As it turns out, Angelico is also in favor and another council member, Brett Cannon, was on board as well. Coghlan's pull request for the PEP was updated to reflect that; it will presumably be merged along with the code changes.

This episode is another example of the Python development process in action. The long beta cycle for releases helps flush out problems, as with the escape sequences issue we looked at last week. In this case, the corner cases were found by MicroPython developers who were adding the walrus operator to their version of the language, so the diversity of language implementations helps find issues as well. Finding those problems in MicroPython ensured that the PEP would be fixed; the discussion around them allowed the exception issue to be worked out. It certainly shows the advantages of having an active community that is actually using and testing beta releases well in advance of the final release.


Index entries for this article
PythonDevelopment model
PythonPEP 572


to post comments

Corner cases and exception types

Posted Aug 13, 2019 21:49 UTC (Tue) by Cyberax (✭ supporter ✭, #52523) [Link] (5 responses)

Just remove the walrus operator entirely. It provides close to no improvements but adds complexity.

Corner cases and exception types

Posted Aug 14, 2019 1:56 UTC (Wed) by xanni (subscriber, #361) [Link]

I disagree. I find that it brings considerable value that more than justifies the added complexity. It's one of the features present in other languages that I have long missed in Python and I'm very pleased to see it added.

Corner cases and exception types

Posted Aug 18, 2019 12:12 UTC (Sun) by robert_s (subscriber, #42402) [Link] (3 responses)

While I can see its use in `if` statements, its use in comprehensions seems to encourage an extremely confusing approach, mutating state in the middle of comprehensions. The power and clarity of comprehension statements comes from their use as functional, pure constructs. All of the examples I've seen of the walrus operator used in comprehensions are confusing at best.

Corner cases and exception types

Posted Aug 21, 2019 20:27 UTC (Wed) by smurf (subscriber, #17840) [Link] (2 responses)

So how would you rewrite the "filtered_data" example? It's plenty concise for me.

Corner cases and exception types

Posted Aug 22, 2019 16:34 UTC (Thu) by nybble41 (subscriber, #55106) [Link]

filtered_data = [y for y in map(f, data) if y is not None]

Corner cases and exception types

Posted Aug 25, 2019 22:43 UTC (Sun) by nevyn (guest, #33129) [Link]

So how would you rewrite the "filtered_data" example? It's plenty concise for me.
Which is:
    filtered_data = [y for x in data if (y := f(x)) is not None]
...the obvious simple choice is:
    # Just iterate data and return the value we want...
    filtered_data = [x for x in iter_f(data) if x is not None]
...which leads to the even better version:
    # Get a list of filtered values we want...
    filtered_data = list_filter_f(data)
...the original idea behind python wasn't to cram everything on a single line to win perl golf competitions.

Corner cases and exception types

Posted Aug 14, 2019 9:08 UTC (Wed) by rsidd (subscriber, #2582) [Link] (2 responses)

Interestingly, in python2 the variable inside a comprehension does leak out. This has bitten me a few times. For example, typing "[0 for i in range(5)]" will set the variable i=4 in python2, but not in python3.

Corner cases and exception types

Posted Aug 14, 2019 9:12 UTC (Wed) by rsidd (subscriber, #2582) [Link] (1 responses)

[replying to myself, sorry] However, this leaking would seem to be a bug in the implementation of := at the language level, not a bug by the programmer. As I understand it, it is very hard to fix in the cpython reference implementation. But therefore turning it into a programming bug seems wrong, and considerably reduces the usefulness of the operator (IMO).

Corner cases and exception types

Posted Aug 14, 2019 16:03 UTC (Wed) by nivedita76 (subscriber, #121790) [Link]

The instances that are being turned into errors don’t make sense. It’s not banning := inside a comprehension, it’s banning it when you try to overwrite an iteration variable with it.

Corner cases and exception types

Posted Aug 14, 2019 9:12 UTC (Wed) by agateau (subscriber, #57569) [Link] (1 responses)

What surprises me here is that the new variable is available in the outer scope. I would expect whatever variable is defined in a comprehension list to stay in the comprehension list.

Corner cases and exception types

Posted Aug 14, 2019 16:01 UTC (Wed) by nivedita76 (subscriber, #121790) [Link]

The leaking out is intentional. The PEP gives examples where it’s useful.


Copyright © 2019, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds