Luc
Luc

Reputation: 6026

Raising an exception in a list comprehension: invalid syntax

To check whether a method is called correctly, I want to check each character and make it impossible for the programmer to use the method incorrectly. Because the method will be called in a web server as return redirect(...), instead of returning an error value (like False or None), I want to raise an exception.

def redirect(uri):
    [raise ValueError('URI must be URL-encoded, ASCII only!') for c in uri if not (32 <= ord(c) <= 127)]

This gives an 'invalid syntax' exception:

File "server.py", line 115
    [raise ValueError('URI must be URL-encoded, ASCII only!') for c in uri if not (32 <= ord(c) <= 127)]
         ^
SyntaxError: invalid syntax

I can work around the problem in various ways, but I wonder: why is raising inside a list comprehension not allowed?

Upvotes: 1

Views: 1129

Answers (1)

jferard
jferard

Reputation: 8180

Syntax

From a syntaxic point of view, your answer is in the full grammar spec.

The raise terminal only appears in rules derived from stmt (statement):

stmt: simple_stmt | compound_stmt
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (... | flow_stmt | ...)
flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
raise_stmt: 'raise' [test ['from' test]]

Whereas the first part of a list comprehension is a test (boolean or expression) or a star_expr (*expr):

atom: ... | '[' [testlist_comp] ']' | ...
testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] )

The point is there is no way a statement can be derived from the (test|star_expr) (left part of a list comprehension). Hence, your expression is syntactically wrong.

Semantic

As pointed by @Neb in a comment, a list comprehension trying to return a raise does not make sense.

You probably remember that print was a statement in Python 2 and became a function in Python 3:

Python 2:

>>> [print(1) for _ in range(1)]
  File "<stdin>", line 1
    [print(1) for _ in range(1)]
         ^
SyntaxError: invalid syntax

Python 3:

>>> [print(1) for _ in range(1)]
1
[None]

The list comprehension is now syntactically correct. Similarly, no syntaxic rule prevents you from writing this:

>>> def raiser(): raise ValueError('URI must be URL-encoded, ASCII only!')
... 
>>> def redirect(uri): [raiser() for c in uri if not (32 <= ord(c) <= 127)]
... 
>>> redirect("abc")
>>> redirect("éàç")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in redirect
  File "<stdin>", line 1, in <listcomp>
  File "<stdin>", line 1, in raiser
ValueError: URI must be URL-encoded, ASCII only!

But the semantic remains unclear: do you want to perform an action (ie use a side effect from a function), or to build a list? Remember that list comprehensions are a borrowing to functional langages, expecially Haskell I think. Hence, they are not here to perform actions.

I quote @Mark Ransom comment to an answer to the "Is it Pythonic to use list comprehensions for just side effects?" question:

I would go even further and state that side effects inside a list comprehension are unusual, unexpected, and therefore evil, even if you're using the resulting list when you're done. – Mark Ransom

I use this rule of thumb: avoid any side effects in list comprehensions, even if you use the result.

Upvotes: 1

Related Questions