In addition to discussing readability, I think performance also matters in some scenarios. A quick timeit benchmark indicates that a test (i.e. “asking permission”) is actually slightly faster than handling the exception (i.e. “asking forgiveness”).
Here’s the code to set up the benchmark, generating a largeish dictionary of random key-value pairs:
setup = """
import random, string
d = {"".join(random.choices(string.ascii_letters, k=3)): "".join(random.choices(string.ascii_letters, k=3)) for _ in range(10000)}
"""
Then the if
test:
stmt1 = """
key = "".join(random.choices(string.ascii_letters, k=3))
if key in d:
_ = d[key]
"""
gives us:
>>> timeit.timeit(stmt=stmt1, setup=setup, number=1000000)
1.6444563979999884
whereas the approach utilizing the exception
stmt2 = """
key = "".join(random.choices(string.ascii_letters, k=3))
try:
_ = d[key]
except KeyError:
pass
"""
gives us:
>>> timeit.timeit(stmt=stmt2, setup=setup, number=1000000)
1.8868465850000575
Interestingly, hoisting the key
generation from the actual benchmark into the setup and therewith looking for the same key over and over, delivers vastly different numbers:
>>> timeit.timeit(stmt=stmt1, setup=setup, number=100000000)
2.3290171539999847
>>> timeit.timeit(stmt=stmt2, setup=setup, number=100000000)
26.412447488999987
I don’t want to speculate whether this emphasizes the benefits of a test vs. exception handling, or if the dictionary buffers the result of the previous lookup and thus biases the benchmark results towards testing…