Better to 'try' something and catch the exception or test if it's possible first to avoid an exception?
Asked Answered
C

8

153

Should I test if something is valid or just try to do it and catch the exception?

  • Is there any solid documentation saying that one way is preferred?
  • Is one way more pythonic?

For example, should I:

if len(my_list) >= 4:
    x = my_list[3]
else:
    x = 'NO_ABC'

Or:

try:
    x = my_list[3]
except IndexError:
    x = 'NO_ABC'

Some thoughts...
PEP 20 says:

Errors should never pass silently.
Unless explicitly silenced.

Should using a try instead of an if be interpreted as an error passing silently? And if so, are you explicitly silencing it by using it in this way, therefore making it OK?


I'm not referring to situations where you can only do things 1 way; for example:

try:
    import foo
except ImportError:
    import baz
Chuckchuckfull answered 29/9, 2011 at 23:55 Comment(0)
A
172

You should prefer try/except over if/else if that results in

  • speed-ups (for example by preventing extra lookups)
  • cleaner code (fewer lines/easier to read)

Often, these go hand-in-hand.


speed-ups

In the case of trying to find an element in a long list by:

try:
    x = my_list[index]
except IndexError:
    x = 'NO_ABC'

the try, except is the best option when the index is probably in the list and the IndexError is usually not raised. This way you avoid the need for an extra lookup by if index < len(my_list).

Python encourages the use of exceptions, which you handle is a phrase from Dive Into Python. Your example not only handles the exception (gracefully), rather than letting it silently pass, also the exception occurs only in the exceptional case of index not being found (hence the word exception!).


cleaner code

The official Python Documentation mentions EAFP: Easier to ask for forgiveness than permission and Rob Knight notes that catching errors rather than avoiding them, can result in cleaner, easier to read code. His example says it like this:

Worse (LBYL 'look before you leap'):

#check whether int conversion will raise an error
if not isinstance(s, str) or not s.isdigit():
    return None
elif len(s) > 10:    #too many digits for int conversion
    return None
else:
    return int(s)

Better (EAFP: Easier to ask for forgiveness than permission):

try:
    return int(s)
except (TypeError, ValueError, OverflowError): #int conversion failed
    return None
Aphonia answered 30/9, 2011 at 0:9 Comment(3)
if index in mylist tests wether index is an element of mylist, not a possible index. You would want if index < len(mylist) instead.Dermatitis
This makes sense, but where do I find the documentation that makes it clear what possible exceptions can be thrown for the int(). docs.python.org/3/library/functions.html#int I am unable to find this info here.Benzedrine
In opposite, you can add this case (from off. doc) to your answer, when you should prefer if/else than try/catchOgg
P
22

In this particular case, you should use something else entirely:

x = myDict.get("ABC", "NO_ABC")

In general, though: If you expect the test to fail frequently, use if. If the test is expensive relative to just trying the operation and catching the exception if it fails, use try. If neither one of these conditions applies, go with whatever reads easier.

Pammie answered 29/9, 2011 at 23:59 Comment(2)
+1 for the explanation under the code sample, which is spot on.Extremadura
I think it's pretty clear this isn't what he was asking, and he's now edited the post to make it even more clear.Nymph
G
10

Using try and except directly rather than inside an if guard should always be done if there is any possibility of a race condition. For example, if you want to ensure that a directory exists, do not do this:

import os, sys
if not os.path.isdir('foo'):
  try:
    os.mkdir('foo')
  except OSError, e
    print e
    sys.exit(1)

If another thread or process creates the directory between isdir and mkdir, you'll exit. Instead, do this:

import os, sys, errno
try:
  os.mkdir('foo')
except OSError, e
  if e.errno != errno.EEXIST:
    print e
    sys.exit(1)

That will only exit if the 'foo' directory can't be created.

Guanase answered 22/9, 2015 at 20:14 Comment(0)
E
8

If it's trivial to check whether something will fail before you do it, you should probably favor that. After all, constructing exceptions (including their associated tracebacks) takes time.

Exceptions should be used for:

  1. things that are unexpected, or...
  2. things where you need to jump more than one level of logic (e.g. where a break doesn't get you far enough), or...
  3. things where you don't know exactly what is going to be handling the exception ahead of time, or...
  4. things where checking ahead of time for failure is expensive (relative to just attempting the operation)

Note that oftentimes, the real answer is "neither" - for instance, in your first example, what you really should do is just use .get() to provide a default:

x = myDict.get('ABC', 'NO_ABC')
Elysian answered 29/9, 2011 at 23:59 Comment(5)
except that if 'ABC' in myDict: x = myDict['ABC']; else: x = 'NO_ABC' is actually often faster than using get, unfortunately. Not saying this is the most important criteria, but it's something to be aware of.Nymph
@agf: Better to write clear and concise code. If something needs to be optimized later, it's easy to come back and rewrite it, but c2.com/cgi/wiki?PrematureOptimizationElysian
I know that; my point was that if / else and try / except can have their place even when there are case-specific alternatives because they have different performance characteristics.Nymph
@agf, do you know if the get() method will be improved in future versions to be (at least) as fast as looking up explicitly? By the way, when looking up twice (as in if 'ABC' in d: d['ABC']), is try: d['ABC'] except KeyError:... not fastest?Aphonia
@Aphonia The slow part of .get() is the attribute lookup and function call overhead at the Python level; using keywords on built-ins basically goes right to C. I don't think it'll be getting too much faster any time soon. As far as if vs. try, read dict.get() method returns a pointer which has some performance info. The ratio of hits to misses matter (try can be faster if the key almost always exists) as does the size of the dictionary.Nymph
O
7

As the other posts mention, it depends on the situation. There are a few dangers with using try/except in place of checking the validity of your data in advance, especially when using it on bigger projects.

  • The code in the try block may have a chance to wreak all sorts of havoc before the exception is caught - if you proactively check beforehand with an if statement you can avoid this.
  • If the code called in your try block raises a common exception type, like TypeError or ValueError, you may not actually catch the same exception you were expecting to catch - it may be something else that raise the same exception class before or after even getting to the line where your exception may be raised.

e.g., suppose you had:

try:
    x = my_list[index_list[3]]
except IndexError:
    x = 'NO_ABC'

The IndexError says nothing about whether it occurred when trying to get an element of index_list or my_list.

Odoacer answered 16/10, 2014 at 17:3 Comment(0)
D
4

Should using a try instead of an if be interpreted as an error passing silently? And if so, are you explicitly silencing it by using it in this way, therefore making it OK?

Using try is acknowledging that an error may pass, which is the opposite of having it pass silently. Using except is causing it not to pass at all.

Using try: except: is preferred in cases where if: else: logic is more complicated. Simple is better than complex; complex is better than complicated; and it's easier to ask for forgiveness than permission.

What "errors should never pass silently" is warning about, is the case where code could raise an exception that you know about, and where your design admits the possibility, but you haven't designed in a way to deal with the exception. Explicitly silencing an error, in my view, would be doing something like pass in an except block, which should only be done with an understanding that "doing nothing" really is the correct error handling in the particular situation. (This is one of the few times where I feel like a comment in well-written code is probably really needed.)

However, in your particular example, neither is appropriate:

x = myDict.get('ABC', 'NO_ABC')

The reason everyone is pointing this out - even though you acknowledge your desire to understand in general, and inability to come up with a better example - is that equivalent side-steps actually exist in quite a lot of cases, and looking for them is the first step in solving the problem.

Delibes answered 30/9, 2011 at 0:7 Comment(0)
A
3

Whenever you use try/except for control flow, ask yourself:

  1. Is it easy to see when the try block succeeds and when it fails?
  2. Are you aware of all side effects inside the try block?
  3. Are you aware of all cases in which the try block throws the exception?
  4. If the implementation of the try block changes, will your control flow still behave as expected?

If the answer to one or more of these questions is 'no', there might be a lot of forgiveness to ask for; most likely from your future self.


An example. I recently saw code in a larger project that looked like this:

try:
    y = foo(x)
except ProgrammingError:
    y = bar(x)

Talking to the programmer it turned that the intended control flow was:

If x is an integer, do y = foo(x).

If x is a list of integers, do y = bar(x).

This worked because foo made a database query and the query would be successful if x was an integer and throw a ProgrammingError if x was a list.

Using try/except is a bad choice here:

  1. The name of the exception, ProgrammingError, does not give away the actual problem (that x is not an integer), which makes it difficult to see what is going on.
  2. The ProgrammingError is raised during a database call, which wastes time. Things would get truly horrible if it turned out that foo writes something to the database before it throws an exception, or alters the state of some other system.
  3. It is unclear if ProgrammingError is only raised when x is a list of integers. Suppose for instance that there is a typo in foo's database query. This might also raise a ProgrammingError. The consequence is that bar(x) is now also called when x is an integer. This might raise cryptic exceptions or produce unforeseeable results.
  4. The try/except block adds a requirement to all future implementations of foo. Whenever we change foo, we must now think about how it handles lists and make sure that it throws a ProgrammingError and not, say, an AttributeError or no error at all.
Areopagus answered 25/8, 2018 at 7:10 Comment(0)
H
0

For a general meaning, you may consider reading Idioms and Anti-Idioms in Python: Exceptions.

In your particular case, as others stated, you should use dict.get():

get(key[, default])

Return the value for key if key is in the dictionary, else default. If default is not given, it defaults to None, so that this method never raises a KeyError.

Helaina answered 30/9, 2011 at 0:9 Comment(2)
I don't think the link covers this situation at all. Its examples are about handling things that represent real errors, not about whether to use exception handling for expected situations.Nymph
First link is outdated.Gawen

© 2022 - 2024 — McMap. All rights reserved.