Thiago Padilha
Thiago Padilha

Reputation: 4650

Why do exceptions/errors evaluate to True in python?

In several places I have to retrieve some value from a dict, but need to check if the key for that value exists, and if it doesn't I use some default value :

    if self.data and self.data.has_key('key'):
        value = self.data['key']
    else:
        value = self.default
    ....

One thing I like about python, is that and/or boolean operators return one of their operands. I'm not sure, but if exceptions evaluated to false, the above code could be rewriten as follows:

    value = self.data['key'] or self.default

I think its intuitive that errors should evaluate to false(like in bash programming). Is there any special reason for python treating exceptions as true?

EDIT:

I just want to show my point of view on the 'exception handling' subject. From wikipedia:

" From the point of view of the author of a routine, raising an exception is a useful way to signal that a routine could not execute normally. For example, when an input argument is invalid (e.g. a zero denominator in division) or when a resource it relies on is unavailable (like a missing file, or a hard disk error). In systems without exceptions, routines would need to return some special error code. However, this is sometimes complicated by the semipredicate problem, in which users of the routine need to write extra code to distinguish normal return values from erroneous ones. "

As I said, raising exceptions are just a fancy way of returning from a function. 'Exception objects' are just a pointer to a data structure that contains information about the error and how that is handled on high level languages depend only on the implementation of the VM. I decided to look at how python 2.6.6 deals with exceptions by looking at output from the 'dis' module:

>>> import dis
>>> def raise_exception():
...     raise Exception('some error')
... 
>>> dis.dis(raise_exception)
  2           0 LOAD_GLOBAL              0 (Exception)
              3 LOAD_CONST               1 ('some error')
              6 CALL_FUNCTION            1
              9 RAISE_VARARGS            1
             12 LOAD_CONST               0 (None)
             15 RETURN_VALUE        

Its clear that python has the exception concept at bytecode level, but even if it didnt, it could still make exception handling constructs behave like they do. Take the following function:

def raise_exception():
...     return Exception('some error')
... 
>>> dis.dis(raise_exception)
  2           0 LOAD_GLOBAL              0 (Exception)
              3 LOAD_CONST               1 ('some error')
              6 CALL_FUNCTION            1
              9 RETURN_VALUE

Both functions create the exception object in the same way as show in 0, 3 and 6. The difference is that the first call the RAISE_VARARGS instruction on the object(and stills returns None) and the second will just return the exception object to calling code. Now take the following bytecode(I'm not sure if this is correct) :

0  LOAD_GLOBAL      (isinstance) #Loads the function 'isinstance'
3  LOAD_GLOBAL      (raise_exception) #loads the function 'raise_exception'
6  CALL_FUNCTION    #call the function raise_exception(which will push an Exception instance to the stack
9  LOAD_GLOBAL      (Exception) #load the Exception class
12 CALL_FUNCTION    #call the 'isinstance function'(which will push 'True to the stack)
15 JUMP_IF_TRUE     (to 27) #Will jump to instruction 33 if the object at the top of stack evaluates to true
18 LOAD_CONS        ('No exceptions were raised')
21 PRINT_ITEM
24 PRINT_NEWLINE    
27 LOAD_CONS        ('Exception caught!')
21 PRINT_ITEM
24 PRINT_NEWLINE

The above translates to something equivalent to this:

if isinstance(raise_exception(), Exception):
    print 'Exception caught!'
else:
    print 'No exceptions were raised'

However, the compiler could generate something like the above instructions when it finds a try block. With this implementation someone could either test a block for an exception or treat functions that return an exception as a 'False' value.

Upvotes: 2

Views: 2581

Answers (5)

Manoj Govindan
Manoj Govindan

Reputation: 74755

Why do exceptions/errors evaluate to True in python?

Instances of Exception evaluate to True (EDIT See @THC4k's comment below. Quite relevant information). That doesn't prevent them from being thrown.

I'm not sure, but if exceptions evaluated to false

In your context it wouldn't suffice that Exceptions evaluate to False. They should also not get thrown and be propagated down the call stack. Rather, they will have to be "stopped" on the spot and then evaluate to False.

I'll leave it to more experienced Pythonistas to comment on why Exceptions do not (or should not) get "stopped" and evaluate to True or False. I'd guess that this is because they are expected to be thrown and propagated. In fact they would cease to be "exceptions" if they were to be stopped and interrogated =P.

but need to check if the key for that value exists, and if it doesn't I use some default value

I can think of two options two ways to get a default value in the absence of a key in a dictionary. One is to use defaultdict. The other is to use the get method.

>>> from collections import defaultdict
>>> d = defaultdict(lambda: "default")
>>> d['key']
'default'

>>> d = dict()
>>> d.get('key', 'default')
'default'
>>> 

PS: if key in dict is preferred to dict.has_key(key). In fact has_key() has been removed in Python 3.0.

Upvotes: 5

Jochen Ritzel
Jochen Ritzel

Reputation: 107666

You had

if self.data and self.data.has_key('key'):
    value = self.data['key']
else:
    value = self.default

This is the same:

if self.data.has_key('key'): # if it has the key, it's not empty anyways
    value = self.data['key']
else:
    value = self.default

Now we write it nicer:

if 'key' in self.data: # `in` does just what you expect
    value = self.data['key']
else:
    value = self.default

It's easy to transform this into a inline if:

value = (self.data['key'] if 'key' in self.data else self.default)

In this case it is simply:

value = self.data.get('key', self.default)

But not if you want to do something like this:

value = (self.data['key'] if 'key' in self.data else self.get_default('key'))

This is different from this

value = self.data.get('key', self.get_default('key'))

because self.get_default('key') will be called unconditionally (before the call to get)!

Upvotes: 2

neil
neil

Reputation: 3635

To answer what you are trying to do, rather than your problem with exceptions, if self.datawas a defaultdictwith a default value of Nonethen if the key wasn't found it would return Nonewhich would evaluate as Falseand then you would get self.defaultas desired.

Note that this would cause problems if the value found in self.datawas 0, False, None or any other value that evaluates as False. If these values might be in there then I think you will have to go with katrielalex's try/except answer.

Upvotes: 0

Katriel
Katriel

Reputation: 123722

You are misunderstanding the use of exceptions. An exception is something gone wrong. It's not just a return value, and it shouldn't be treated as such.

Explicit is better than implicit.

Because an exception is raised when something goes wrong, you must explicitly write code to catch them. That's deliberate -- you shouldn't be able to ignore exceptions as merely False, because they're more than that.

If you swallowed exceptions as you seem to suggest, you wouldn't be able to tell when code went wrong. What would happen if you referenced an unbound variable? That should give you a NameError, but it would give you... False?!


Consider your code block:

value = self.data['key'] or self.default

You want self.data['key'] to return False if key is not a key of self.data. Do you see any problems with this approach? What if self.data == {'key': False}? You couldn't distinguish between the case of False being stored in the dictionary and False being returned because of a missing key.

Further, if this were changed more generally, so that all exceptions were swallowed (!), how would you distinguish between a KeyError ('key' not in self.data) and say a NameError (self.data not defined)? Both would evaluate to False.

Explicitly writing code to catch the exception solves this problem, because you can catch exactly those exception that you want:

try:
    value = self.data['key']
except KeyError:
    value = self.default

There is a data structure already written that does this for you, though, because default dictionaries are very commonly needed. It's in collections:

>>> import collections
>>> data = collections.defaultdict(lambda: False)
>>> data['foo'] = True
>>> data['foo']
True
>>> data['bar']
False

Upvotes: 9

Ivo
Ivo

Reputation: 3589

You can use get:

value = self.data.get('key', self.default)

Update: You are misinterpreting the or keyword there. The self.default value is only used if self.data['key'] evaluates to False, not if 'key' does not exist in self.data. If self.data contains no 'key', an exception is still raised.

In the expression:

self.data['key'] or self.default

the Python interpreter will evaluate self.data['key'] first, then check whether it evaluates to True. If it is True, then it is the result of the entire expression. If it is false, self.default is the result of the expression. However, in evaluating self.data['key'], 'key' is not in self.data, then an exception is raised, and the evaluation of the entire expression is aborted, and indeed the containing block until a matching except block is found somewhere along the stack. The assignment is also not executed in case an exception is raised, and value remains at whatever initial value it had.

Upvotes: 3

Related Questions