6

Why is "Easier to Ask Forgiveness than it is to get Permission" (EAFP) considered good practice in Python? As a programming novice I have the impression that using many try...except routines will rather lead to bloated and less readable code than compared to using other checks.

What is the advantage of the EAFP approach?

NB: I know there are similar questions here, but they mostly refer to some specific example, while I am more interested in the philosophy behind the principle.

n1000
  • 5,058
  • 10
  • 37
  • 65

4 Answers4

5

LBYL, the counter approach to EAFP doesn't have anything to do with assertions, it just means that you add a check before you try to access something that might not be there.

The reason that Python is EAFP is that unlike other languages (Java for example) - in Python catching exceptions is relatively inexpensive operation, and that's why you're encouraged to use it.

Example for EAFP:

try:
    snake = zoo['snake']
except KeyError as e:
    print "There's no snake in the zoo"
    snake = None

Example for LBYL:

if 'snake' in zoo:
    snake = zoo['snake']
else:
    snake = None
Nir Alfasi
  • 53,191
  • 11
  • 86
  • 129
  • Note that the second one should instead use a unique sentinel object, otherwise it will fail on any false value. – Ignacio Vazquez-Abrams Oct 02 '15 at 07:07
  • @IgnacioVazquez-Abrams you are right, but in this example I think I can safely assume that `False`, `0` or any other falsy value will not represent a snake in the zoo :) – Nir Alfasi Oct 02 '15 at 07:08
  • If you are using `dict.get`, then you should just pass `None` as its default value: `zoo.get('snake', None)`. This is not really a good example for LBYL due to the use of the forgiving `dict.get`. – poke Oct 02 '15 at 07:09
  • @poke true, it can be shortened into: `snake = zoo.get('snake', None)` but the point was to show an example of LBYL not how to "work around it" :) – Nir Alfasi Oct 02 '15 at 07:11
  • Then do the “looking” part right and check whether the key exists in the dictionary without forgivingly getting a value from it and checking the truthness of that value. – poke Oct 02 '15 at 07:15
  • "The reason that Python is EAFP is that unlike other languages (Java for example) - in Python catching exceptions is an inexpensive operation" : well, that's just wrong. Setting up a try/except block is cheap but catching the exception is not. Try this code to check it out https://gist.github.com/RealBigB/38f4579c543261f1400f - here (python 2.7.x) I get 1.3156080246 for eafp and 0.368113994598 for lbyl. – bruno desthuilliers Oct 02 '15 at 07:24
  • 2
    @brunodesthuilliers do you consider an example that throws a million exceptions and catches them as a good example that can be used as a model for a "normal" program ? :) – Nir Alfasi Oct 02 '15 at 07:30
  • 2
    @brunodesthuilliers As I tried to explain in my answer, catching exceptions in Python is cheaper compared to other languages, but of course it’s not free. Exceptions are still for exceptional cases, in this case, if you expect the key to exist most of the time. Then, the additional check for LBYL is slightly more expensive. – poke Oct 02 '15 at 07:31
  • @brunodesthuilliers take your example: (1.3156080246-0.368113994598)/1000000 = 0.00000094749403 which is the average cost for catching one exception in the example that you gave. That's less than a microsecond, or 947 nano seconds if to be more accurate... – Nir Alfasi Oct 02 '15 at 07:35
  • @alfasin wrt/ the code it's just your very own example - and stating that "in Python catching exceptions is an inexpensive operation" is plain misleading. It might be cheaper than in some other languages but it still way more expensive than a key lookup. – bruno desthuilliers Oct 02 '15 at 07:46
  • @brunodesthuilliers you take my words out of the context, it's inexpensive comparing to other languages and thus encouraged to be used. I did not say it's 'free'. – Nir Alfasi Oct 02 '15 at 07:47
  • 2
    @brunodesthuilliers But it *is* an inexpensive operation. That doesn’t mean you can compare it against something else that is even cheaper and say that it’s wrong. It’s like saying `print` is expensive because not printing is a lot cheaper. Try to run the same example in e.g. Java and you can see why exceptions are inexpensive here. – poke Oct 02 '15 at 07:48
  • I once had to fix someone's else code for a somewhat complex daily computing task that was tooking way too much time. He used EAFP all over the place. Just changing this to LBYL almost halved the execution time. In the above example, the EAFP version is only 3.5x times slower, yeah sure that's inexpensive. And that will be my last comment on this. – bruno desthuilliers Oct 02 '15 at 07:55
  • @brunodesthuilliers this "3.5x slower" makes it 1 second slower per **million** executions. When you "improved" his code you must have done other things that you're not even aware of... – Nir Alfasi Oct 02 '15 at 08:00
5

You are mixing two things here: Assertions and EAFP-based logic.

Assertions are used to validate the contract of functions, i.e. its pre- and postconditions and sometimes also its invariants. They ensure that a function would be used in the way it should be used. They are not for code flow though, since they completely interrupt the execution on error. A common example is a check for None arguments in function calls.

In Python, you usually avoid using assertions too much. In general, you should expect users of your code to use it correctly. For example, if you document a function to take an argument that is not None, then it’s not necessary to have an assert that validates that. Instead, just expect that there is a value. If there is an error because of a None value, then it will bubble up anyway, so the user knows that they did something wrong. But you shouldn’t have to check everything all the time.

Now, EAFP is something different. It’s used in control flow, or rather, it avoids additional control flow in favor of expecting things to be correct and catching exceptions instead if they are not. A common example that shows the difference is a key access in a dictionary:

# LBYL
if key in dic:
    print(dic[key])
else:
    handleError()

# EAFP
try:
    print(dic[key])
except KeyError:
    handleError()

Now this looks very similar, although you should keep in mind that the LBYL solution checks the dictionary twice. As with all code that catches exceptions, you should only do it if the non-existance of a key is the exceptional case. So if usually, the supplied key is excepted to be in the dictionary, then it’s EAFP and you should just access it directly. If you don’t expect the key to be present in the dictionary, then you should probably check its existance first (while exceptions are cheaper in Python, they are still not free, so keep them for exceptional cases).

A benefit of EAFP here would also be that deeper down in the logic of your library or application, where the key comes from above, you can just assume that a valid key was passed here. So you don’t need to catch exceptions here but just let them bubble up to a higher point in your code where you can then handle the error. That allows you to have lower-level functions completely free of these kind of checks.

poke
  • 369,085
  • 72
  • 557
  • 602
  • Thank you. Big +1 for illustrating and explaining the different advantages of the EAFP approach in Python. – n1000 Oct 02 '15 at 07:18
1

Nice question! There are very few questions in StackOverflow asking about "the philosophy behind the principle".

Regarding to the EAFP definition in Python glossary, I would even go further and say its mentioning of "caches exceptions if the assumption proves false" is somewhat misleading in this context. Because, let's face it, the following 2nd code snippet does NOT look more "clean and fast" (a term used in the aforementioned definition). No wonder the OP asked this question.

# LBYL
if key in dic:
    print(dic[key])
else:
    handleError()

# EAFP
try:
    print(dic[key])
except KeyError:
    handleError()

I would say, the real moment when EAFP shines, is that you do NOT write try ... except ... at all, at least not in most of your underlying code base. Because, the first rule of exception handling: do not do exception handling. With that in mind, now, let's rewrite the 2nd snippet into this:

# Real EAFP
print(dic[key])

Now, isn't that real EAFP approach clean and fast?

RayLuo
  • 17,257
  • 6
  • 88
  • 73
0

I'll expand on the answer from @RayLuo.

The problem with LBYL is, it actually doesn't work in general. Unless you have a single-threaded application, there's always a possible race condition:

# LBYL
if key in dic:
    # RACE CONDITION
    print(dic[key])
else:
    handleError()

# EAFP
try:
    print(dic[key])
except KeyError:
    handleError()

A key can be added to the dict between the if check and and print. That doesn't sound incredibly likely in this particular case. However it's much more likely if the check you're making is against a DB, API, or any external data source that can change asynchronously with your application data.

That means the "right" way to implement LBYL is:

if key in dic:
    try:
        print(dic[key])
    except KeyError:
        handleError()
else:
    handleError()

Note the try / except clause is exactly the same as the EAFP approach.

Since you have to handle the exception EAFP-style even when using the LBYL approach, you might as well use the EAFP approach in the first place.

The only circumstance in which I'd use the if check is if the action that follows (print in this case) is very expensive / time-consuming to initiate. That would be rare and not a reason to use the if check every time.

Bottom line: LBYL doesn't work in the general case, but EAFP does. Good developers focus on general solution patterns they can use confidently across a wide range of problems. Learn to use EAFP consistently.

Chris Johnson
  • 20,650
  • 6
  • 81
  • 80