I was discussing the wisdom of using try/except throughout Python code today with someone and there were a couple of points that I felt would be quick and easy to verify or debunk.
I wrote a quick little script to time a a set of functions testing the use cases of not knowing whether an element in a dictionary exists or not prior to referencing it. There are a set where it doesn’t exist and set where it does. The numbers are interesting:
The case where the key does not exist:
1,000 iterations:
with_try (1.562 ms)
with_try_exc (2.166 ms)
without_try (0.233 ms)
without_try_not (0.201 ms)
100,000 iterations:
with_try (168.793 ms)
with_try_exc (223.589 ms)
without_try (24.877 ms)
without_try_not (20.992 ms)
1,000,000 iterations:
with_try (1571.420 ms)
with_try_exc (2228.899 ms)
without_try (250.723 ms)
without_try_not (219.819 ms)
The case where the key does exist:
1,000 iterations:
exists_with_try (0.154 ms)
exists_with_try_exc (0.141 ms)
exists_without_try (0.216 ms)
exists_without_try_not (0.220 ms)
100,000 iterations:
exists_with_try (15.647 ms)
exists_with_try_exc (15.165 ms)
exists_without_try (22.302 ms)
exists_without_try_not (23.364 ms)
1,000,000 iterations:
exists_with_try (158.330 ms)
exists_with_try_exc (158.038 ms)
exists_without_try (233.005 ms)
exists_without_try_not (237.813 ms)
From these results, I think it is fair to quickly determine a number of conclusions:
- If there is a high likelihood that the element doesn’t exist, then you are better off checking for it with
has_key. - If you are not going to do anything with the Exception if it is raised, then you are better off not putting one have the
except - If it is likely that the element does exist, then there is a very slight advantage to using a try/except block instead of using
has_key, however, the advantage is very slight.
There is obviously a lot missing from this analysis and it really doesn’t provide anything of earth-shattering revelation — using exception handling logic where you don’t expect to encounter many exceptions is going to be more expensive. It just seemed like a good excuse to write a little Python.
For those interested here is the script I wrote to arrive at the numbers above:
import time
def time_me(function):
def wrap(*arg):
start = time.time()
r = function(*arg)
end = time.time()
print "%s (%0.3f ms)" % (function.func_name, (end-start)*1000)
return r
return wrap
# Not Existing
@time_me
def with_try(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
try:
get = d['notexist']
except:
pass
@time_me
def with_try_exc(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
try:
get = d['notexist']
except Exception, e:
pass
@time_me
def without_try(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
if d.has_key('notexist'):
pass
else:
pass
@time_me
def without_try_not(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
if not d.has_key('notexist'):
pass
else:
pass
# Existing
@time_me
def exists_with_try(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
try:
get = d['somekey']
except:
pass
@time_me
def exists_with_try_exc(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
try:
get = d['somekey']
except Exception, e:
pass
@time_me
def exists_without_try(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
if d.has_key('somekey'):
pass
else:
pass
@time_me
def exists_without_try_not(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
if not d.has_key('somekey'):
pass
else:
pass
print "The case where the key does not exist:"
print "1,000 iterations:"
with_try(1000)
with_try_exc(1000)
without_try(1000)
without_try_not(1000)
print "\n100,000 iterations:"
with_try(100000)
with_try_exc(100000)
without_try(100000)
without_try_not(100000)
print "\n1,000,000 iterations:"
with_try(1000000)
with_try_exc(1000000)
without_try(1000000)
without_try_not(1000000)
print "\n\nThe case where the key does exist:"
print "1,000 iterations:"
exists_with_try(1000)
exists_with_try_exc(1000)
exists_without_try(1000)
exists_without_try_not(1000)
print "\n100,000 iterations:"
exists_with_try(100000)
exists_with_try_exc(100000)
exists_without_try(100000)
exists_without_try_not(100000)
print "\n1,000,000 iterations:"
exists_with_try(1000000)
exists_with_try_exc(1000000)
exists_without_try(1000000)
exists_without_try_not(1000000)
Update: Recently I updated my blog to process posts as if they were written in markdown. Just updating the raw post to markdown syntax.












6 comments ↓
I tried this myself one time and got similar results. Here’s my one bit of advice though:
d.haskeyis not very Pythonic. Do"somekey" in dinstead. In fact, in Python 3, thehaskey methodwill go away andinwill be the only way to find out if a key is in a dictionary.I think it’s important to note that you forgot .get() as an option. A non-exception-throwing technique, calling d.get(’something’) will return None (or an optional default you provide as a second parameter). It will perform near identically in both the existent and non-existent cases (very tight C code). Frequently, code can be restructured to use this.
The conventional wisdom has always been that when dereferencing a dict, if the likelihood is largely in favor of the item being there (say, 99 out of 100), the try/except method is often fastest (even more so than .get()).
I’m also curious, since it is closer to best practices, what would happen if you caught KeyError instead of the too-general Exception since that’s what most people do. For example, your code wouldn’t exit if someone managed to mash Ctrl-C during the dict dereference since KeyboardInterrupt would be caught and ignored.
Finally, in the universe of optimization, going after a constant increase (in this case about 2x) is frequently a wasted optimization. Fire up the profiler and go find the part of code that takes n^2 to 10x time than it needs to.
Pretty cool though!
Don’t use dict.has_key(). Use the “in” operator instead, like ‘key in dict’. It’s easier to read, more Pythonic, and faster.
You should be using ‘key in d’ instead of ‘d.has_key(key)’ as it’s actually a little faster — it avoids the method lookup.
Py3k is removing has_key mostly for this reason
I’m learning Python, so thanks to all for pointing out not to use haskey(). Sadly, Learning Python, 3rd Edition, uses haskey() in its example in the dictionary section.
Interested in Kyle’s comments on targeting specific exceptions, I added a KeyError function to your example (one run shown below). I ran it a number of times and found that the KeyError catch block was always faster than Exception, whether or not a KeyError exception was raised.
I ran it using both Python 2.4.1 and 2.5 (just because I use both versions) and found no significant difference.
The case where the key does not exist:
1,000,000 iterations: withtry (2347.707 ms) withtryexc (3640.913 ms) withtrykeyerror (3097.357 ms) withouttry (500.057 ms) withouttrynot (538.282 ms)
The case where the key does exist:
1,000,000 iterations: existswithtry (350.723 ms) existswithtryexc (352.511 ms) existswithtrykeyerror (344.870 ms) existswithouttry (484.887 ms)
existswithouttry_not (552.334 ms)
Leave a Comment