1

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?

Luc
  • 5,339
  • 2
  • 48
  • 48
  • What would it mean for you having a list with a raised exception as element? – Neb Mar 06 '19 at 10:11
  • @Neb Doesn't the `raise` keyword raise the exception? It would never end up in the list because it breaks out of that function until it finds an `except` statement. To have exceptions as elements in the list, I would have used `[ValueError(...) for c in uri if ...`]). – Luc Mar 06 '19 at 10:25
  • Yes, it does. But, i'm wondering, why do you want it inside a list? Can't you raise it without putting it in a list? – Neb Mar 06 '19 at 10:26
  • @Neb Sure, one could do `for c in uri:\n\tif ...:\n\t\traise ValueError(...)`, but I'm wondering *why* raise is not allowed in a list comprehension. – Luc Mar 06 '19 at 11:15
  • To know why, look at this answer https://stackoverflow.com/questions/1528237/how-can-i-handle-exceptions-in-a-list-comprehension-in-python. – Neb Mar 06 '19 at 11:25
  • However, i find completely unintuitive to build a list comprehension with a raised exception. Raised exceptions interrupt the flow of you program, hence whatever your list will contain, it would be thrown away as soon as the exception is raised. – Neb Mar 06 '19 at 11:26
  • @Neb That link indeed answers my question, particularly the part "a list comprehension is an expression containing other expression, nothing more (i.e., no statements". Do you want to post that as an answer (that list comprehensions cannot contain statements, such as raise)? – Luc Mar 06 '19 at 11:29
  • No, it would be repetitive as your question has already an answer elsewhere :) – Neb Mar 06 '19 at 11:41
  • @Neb The answer mentions it more as an aside than as answer to the question. There it's a reason why you can't catch something in a list comprehension ("it would have to be a statement"), rather than the reason why you can't use raise in a list comprehension ("statements are not allowed in comprehensions"). If you post the answer, I'll accept it; if not, I'm not sure yet: maybe I'll close vote as duplicate (it is closely related, even if not identical), or maybe I'll answer my own question. – Luc Mar 06 '19 at 12:43

1 Answers1

1

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.

jferard
  • 7,835
  • 2
  • 22
  • 35