It looks like this has changed significantly since the original post 15 years ago, and especially with the "'Zero-cost' exceptions" in SuperNova's answer. For my current project, I care more about lookup speed and errors than 1 / 0
errors, so I'm looking into that. I found this blog post doing exactly what I wanted, but in Python 2.7. I updated the test to 3.13, (Windows 10, i9-9900k) with results below.
This compares checking key existence with if key in d
to using a try: except:
block
'''
The case where the key does not exist:
100 iterations:
with_try (0.016 ms)
with_try_exc (0.016 ms)
without_try (0.003 ms)
without_try_not (0.002 ms)
1,000,000 iterations:
with_try (152.643 ms)
with_try_exc (179.345 ms)
without_try (29.765 ms)
without_try_not (32.795 ms)
The case where the key does exist:
100 iterations:
exists_unsafe (0.005 ms)
exists_with_try (0.003 ms)
exists_with_try_exc (0.003 ms)
exists_without_try (0.005 ms)
exists_without_try_not (0.004 ms)
1,000,000 iterations:
exists_unsafe (29.763 ms)
exists_with_try (30.970 ms)
exists_with_try_exc (30.733 ms)
exists_without_try (46.288 ms)
exists_without_try_not (46.221 ms)
'''
where it looks like the try
block has a very small overhead, where if the key exists, an unsafe check and try
check are the same. Using in
has to hash the key for the check, and again for the access, so it slows by ~30% with the redundant operation for real usage. If the key does not exist, the try
costs 5x the in
statement, which is the same cost for either case.
So, it does come back to asking if you expect few errors, use try
and many use in
And here's the code
import time
def time_me(function):
def wrap(*arg):
start = time.time()
r = function(*arg)
end = time.time()
print("%s (%0.3f ms)" % (function.__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 as e:
pass
@time_me
def without_try(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
if 'notexist' in d:
pass
else:
pass
@time_me
def without_try_not(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
if not 'notexist' in d:
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_unsafe(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
get = d['somekey']
@time_me
def exists_with_try_exc(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
try:
get = d['somekey']
except Exception as e:
pass
@time_me
def exists_without_try(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
if 'somekey' in d:
get = d['somekey']
else:
pass
@time_me
def exists_without_try_not(iterations):
d = {'somekey': 123}
for i in range(0, iterations):
if not 'somekey' in d:
pass
else:
get = d['somekey']
print("The case where the key does not exist:")
print("100 iterations:")
with_try(100)
with_try_exc(100)
without_try(100)
without_try_not(100)
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("100 iterations:")
exists_unsafe(100)
exists_with_try(100)
exists_with_try_exc(100)
exists_without_try(100)
exists_without_try_not(100)
print("\n1,000,000 iterations:")
exists_unsafe(1000000)
exists_with_try(1000000)
exists_with_try_exc(1000000)
exists_without_try(1000000)
exists_without_try_not(1000000)