Why does using `or` within an except clause not cause a SyntaxError? Is there a valid use for it?

Loï*_*ira 11 python error-handling

At work, I stumbled upon an except clause with an or operator:

try:
    # Do something.
except IndexError or KeyError:
    # ErrorHandling
Run Code Online (Sandbox Code Playgroud)

I know the exception classes should be passed as a tuple, but it bugged me that it wouldn't even cause a SyntaxError.

So first I wanted to investigate whether it actually works. And it doesn't.

>>> def with_or_raise(exc):
...     try:
...         raise exc()
...     except IndexError or KeyError:
...         print('Got ya!')
...

>>> with_or_raise(IndexError)
Got ya!

>>> with_or_raise(KeyError)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in with_or_raise
KeyError
Run Code Online (Sandbox Code Playgroud)

So it did not catch the second exception, and looking at the bytecode, it becomes clearer why:

>>> import dis
>>> dis.dis(with_or_raise)
  2           0 SETUP_EXCEPT            10 (to 12)

  3           2 LOAD_FAST                0 (exc)
              4 CALL_FUNCTION            0
              6 RAISE_VARARGS            1
              8 POP_BLOCK
             10 JUMP_FORWARD            32 (to 44)

  4     >>   12 DUP_TOP
             14 LOAD_GLOBAL              0 (IndexError)
             16 JUMP_IF_TRUE_OR_POP     20
             18 LOAD_GLOBAL              1 (KeyError)
        >>   20 COMPARE_OP              10 (exception match)
             22 POP_JUMP_IF_FALSE       42
             24 POP_TOP
             26 POP_TOP
             28 POP_TOP

  5          30 LOAD_GLOBAL              2 (print)
             32 LOAD_CONST               1 ('Got ya!')
             34 CALL_FUNCTION            1
             36 POP_TOP
             38 POP_EXCEPT
             40 JUMP_FORWARD             2 (to 44)
        >>   42 END_FINALLY
        >>   44 LOAD_CONST               0 (None)
             46 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

So we can see, instruction 14 first loads the IndexError class onto the stack. Then it checks whether that value is True, which it is because of Python truthiness and finally jumps directly to instruction 20 where the exception match is done. Since instruction 18 was skipped, KeyError was never loaded onto the stack and therefore doesn't match.

I tried with Python 2.7 and 3.6, same result.

But then, why is it valid syntax? I imagine it being one of the following:

  1. It's an artifact from a really old version of Python.
  2. There is actually a valid use case for using or within an except clause.
  3. It's simply a limitation of the Python parser which might have to accept any expression after the except keyword.

My vote is on 3 (given I saw some discussion about a new parser for Python) but I'm hoping someone can confirm that hypothesis. Because if it was 2 for example, I want to know that use case!

Also, I'm a bit clueless on how I'd continue that exploration. I imagine I would have to dig into CPython parser's source code but idk where to find it and maybe there's an easier way?

dec*_*eze 7

In except e, e can be any valid Python expression:

try1_stmt ::=  "try" ":" suite
               ("except" [expression ["as" identifier]] ":" suite)+
               ...
Run Code Online (Sandbox Code Playgroud)

[..] For an except clause with an expression, that expression is evaluated, and the clause matches the exception if the resulting object is “compatible” with the exception. An object is compatible with an exception if it is the class or a base class of the exception object or a tuple containing an item compatible with the exception.

https://docs.python.org/3/reference/compound_stmts.html#the-try-statement

The expression IndexError or KeyError yields the value IndexError. So this is equivalent to:

except IndexError:
   ...
Run Code Online (Sandbox Code Playgroud)