dahpgjgamgan
dahpgjgamgan

Reputation: 3007

Why raising a tuple works if first element is an Exception?

I have a hard time figuring this one out, it's about mistakes that can be done when raising an exception in Python 2.7:

try:
  raise [1, 2, 3, 4]
except Exception as ex:
  print ex

the message here is "exceptions must be old-style classes or derived from BaseException, not list" - This part is ok, but when I change it to tuple, I am getting confused:

try:
  raise (1, 2, 3, 4)
except Exception as ex:
  print ex

the message here is "exceptions must be old-style classes or derived from BaseException, not int" - why is it interpreted as raising an int, not a tuple?

Futhermore:

try:
  raise (Exception, 'a message')
except Exception as ex:
  print ex

Here we are actually rising an Exception (consistent behaviour when compared with previous example, where we were raising an int) - I briefly thought that this is just an alternate way for this:

try:
  raise Exception, 'a message'
except Exception as ex:
  print ex

But in this case, 'a message' is being passed to Exceptions ctor (as documented on docs.python.org)

Can someone explain the 2nd and 3rd cases, and possible point me to code in interpreter that is responsible for this?

Upvotes: 24

Views: 6680

Answers (3)

Martijn Pieters
Martijn Pieters

Reputation: 1124070

As documented in the Python 2 reference, the raise statement takes up to 3 expressions to create the exception being raised:

raise_stmt ::= "raise" [expression ["," expression ["," expression]]]

If the first expression is a tuple, python will 'unwrap' the tuple recursively, taking the first element until it finds something other than a tuple. This behavior is being removed from Python 3 (see PEP 3109). The following is legal:

>>> raise ((Exception, 'ignored'), 'ignored'), 'something', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: something

The documentation explains the rest in more detail, but the raise statement expects the first value to be a Exception class, the second value is seen as the value of the exception (the message) and the third value is a traceback. Python fills in None for the latter two values if missing.

If the first value is a instance instead, the second value must be None:

>>> raise Exception('something'), 'something', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: instance exception may not have a separate value

If you use a tuple of more than 3 items, it'll raise a syntax error:

>>> raise Exception, 'something', None, None
  File "<stdin>", line 1
    raise Exception, 'something', None, None
                                      ^
SyntaxError: invalid syntax

In your case however, you raised neither a class nor an instance, so that's what Python found to be incorrect first; if I use a string it'll complain too:

>>> raise 'not an exception', 'something', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: exceptions must be old-style classes or derived from BaseException, not str

The correct syntax is of course:

>>> raise Exception, 'something', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: something

Upvotes: 19

mutantacule
mutantacule

Reputation: 7073

Apparently Python accepts also a non-empty tuple for the first expression in a raise statement despite the documentation (but as stated in this PEP), and if it's a tuple, it uses recursively its first element for the class of the exception. Let me show you some code:

>>> raise ValueError, 'sdf', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: sdf

>>> raise (ValueError, 5), 'sdf', None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: sdf

Despite what I've said in my previous comment, there is no auto-unpacking, because the string is not passed to the exception class in my next example:

>>> raise (ValueError, 'sdf', None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError

Also using the python ast module, we can see that in a raise expression there is no tuple by default:

>>> ast.dump(ast.parse('raise ValueError, "asd"'))
"Module(body=[Raise(type=Name(id='ValueError', ctx=Load()), inst=Str(s='asd'), tback=None)])"

And if we use a tuple, that's passed as the type argument:

>>> ast.dump(ast.parse('raise (ValueError, "asd")'))
"Module(body=[Raise(type=Tuple(elts=[Name(id='ValueError', ctx=Load()), Str(s='asd')], ctx=Load()), inst=None, tback=None)])"

Upvotes: 3

georg
georg

Reputation: 215029

http://docs.python.org/reference/simple_stmts.html#the-raise-statement

"raise" [expression ["," expression ["," expression]]]

If no expressions are present, raise re-raises the last exception that was active in the current scope... Otherwise, raise evaluates the expressions to get three objects, using None as the value of omitted expressions. The first two objects are used to determine the type and value of the exception.

Actually, I thought python does tuple unpacking here

try:
    raise (ValueError, "foo", ), "bar"
except Exception as e:
    print e.message # foo or bar?

but if it did, the result would be "foo", and not "bar". This behavior doesn't appear to be documented anywhere, there's only a short note about it being dropped in py3:

In Python 2, the following raise statement is legal

raise ((E1, (E2, E3)), E4), V

The interpreter will take the tuple's first element as the exception type (recursively), making the above fully equivalent to

raise E1, V

As of Python 3.0, support for raising tuples like this will be dropped. This change will bring raise statements into line with the throw() method on generator objects, which already disallows this.

http://www.python.org/dev/peps/pep-3109/#id17

Upvotes: 3

Related Questions