Exception Handling¶
Exceptions are a really nifty Python feature – really handy!
From the zen:
“Errors should never pass silently.”
“Unless explicitly silenced.”
That’s what exception handling is all about.
Exceptions¶
An “Exception” is an indication that something out of the ordinary (exceptional) happened.
Note that they are NOT called “Errors” – often they are, but often it’s not an indication of an error per se.
This is why we have exception handling – because we often know that exceptions will occur, and know how to handle them – we don’t want the program to crash out.
Handling Exceptions¶
Exceptions are handled with a “try – except” block.
This provides another branching structure (kind of like if) – a way for different code to run depending on what happens.
try:
do_something()
f = open('missing.txt')
process(f) # never called if file missing
except FileNotFoundError:
print("Couldn't find missing.txt")
Bare except
¶
Never do this:
try:
do_something()
f = open('missing.txt')
process(f) # never called if file missing
except:
print "couldn't find missing.txt"
If you don’t specify a particular exception, except
will catch All exceptions.
Always capture the particular Exception(s) you know how to handle.
Trust me, you can’t anticipate everything, and you want the exception to propagate if it is not expected when you wrote the code.
Testing for errors “by hand”:¶
Use Exceptions, rather than your own tests:
Don’t do this:
do_something()
if os.path.exists('missing.txt'):
f = open('missing.txt')
process(f)
It will almost always work – but the almost will drive you crazy.
It is “possible” that the file got deleted by another process in the precise moment between checking for it and opening it. Rare, but possible. Catching the exception will always work – even in that rare case.
Example from mailroom exercise:¶
You want to convert the user’s input into an integer. And you want to give a nice message if the user didn’t provide a valid input.
So you could do this:
if num_in.isdigit():
num_in = int(num_in)
But – int(num_in)
will only work if the string can be converted to an integer.
So you can also do:
try:
num_in = int(num_in)
except ValueError:
print("Input must be an integer, try again.")
continue
This is particularly helpful for things like converting to a float – much more complicated to check – and all that logic is already in the float()
constructor.
Or let the Exception be raised if you can’t handle it.
EAFP¶
This is all an example of the EAFP principle:
“It’s Easier to Ask Forgiveness than Permission”
– Grace Hopper
The idea is that you want to try to do what you want to do – and then handle it if it doesn’t work (forgiveness).
Rather than check to see if you can do it before trying (permission).
Here’s a nice PyCon talk by Alex Martelli about that:
http://www.youtube.com/watch?v=AZDWveIdqjY
(Alex Martelli is a Python Luminary – read / watch anything you find by him).
Do you catch all Exceptions?¶
For simple scripts, let exceptions happen.
Only handle the exception if the code can and will do something (useful) about it.
This results in much better debugging info when an error does occur. The user will see the exception, and where in the code it happened, etc.
Exceptions – finally
¶
There is another component to exception handling control structures:
try:
do_something()
f = open('missing.txt')
process(f) # never called if file missing
except FileNotFoundError:
print("couldn't open missing.txt")
finally:
do_some_clean-up
The code in the finally:
clause will always run.
This is really important if your code does anything before the exception occurred that needs to be cleaned up – open database connection, etc…
NOTE: In the above example, you can often avoid all that exception handling code using a with statement:
with open('missing.txt') as f:
process(f)
In this case, the file will be properly closed regardless. And many other systems, like database managers, etc. can also be used with with
.
This is known as a “context manager”, and was added to Python specifically to handle the common cases that required finally
clauses. But if your use case does not already have a context manager that handles the cleanup you may need.
Exceptions – else
¶
Yet another flow control option:
try:
do_something()
f = open('missing.txt')
except IOError:
print("couldn't open missing.txt")
else:
process(f) # only called if there was no exception
So the else
block only runs if there was no exception. That was also the case in the previous code, so what’s the difference?
Advantage of else
:
Using the else
block lets you catch the exception as close to where it occurred as possible – always a good thing.
Why? – because maybe the process(f)
could raise an exception, too? Then you don’t know if the exception came from the open()
call or in some code after that.
This bears repeating:
Always catch exceptions as close to where they might occur as you can.
Exceptions – using the Exception object¶
What can you do in an except
block?
If your code can continue along fine, you can do very little and move along:
try:
do_something()
except ValueError:
print("That wasn't any good")
And that’s that.
But if your code can’t continue on, you can re-raise the exception:
try:
do_something()
except ValueError:
print("That wasn't any good")
raise
The raise
statement will re-raise the same exception object, where it may get caught higher up in the code, or even end the program.
Exception objects are full-fledged Python objects – they can contain data, and you can add data to them. You can give a name to a raised Exception with as
:
try:
do_something()
f = open('missing.txt')
except IOError as the_error:
print(the_error)
the_error.extra_info = "some more information"
raise
This prints the exception, then adds some extra information to it, and then re-raises the same exception object – so it will have that extra data when it gets handled higher up on the stack.
This is particularly useful if you catch more than one exception:
except (IOError, BufferError, OSError) as the_error:
do_something_with(the_error)
You may want to do something different depending on which exception it is. And you can inspect the Exception object to find out more about it. Each Exception has different information attached to it – you’ll need to read its docs to see.
For an example – try running this code:
In [34]: try:
...: f = open("blah")
...: except IOError as err:
...: print(err)
...: print(dir(err))
...: the_err = err
The print(dir(err))
will print all the names (attributes) in the error object. A number of those are ordinary names that all objects have, but a few are specific to this error.
the the_err = err
line is there so that we can keep a name bound to the err
after the code is run. err
as bound by the except line only exists inside the following block.
Now that we have a name to access it, we can look at some of its attributes. The name of the file that was attempted to be opened:
In [35]: the_err.filename
Out[35]: 'blah'
The message that will be printed is usually in the .args
attribute:
In [37]: the_err.args
Out[37]: (2, 'No such file or directory')
the .__traceback__
attribute hold the actual traceback object – all the information about the context the exception was raised in. That can be inspected to get all sorts of info. That is very advanced stuff, but you can investigate the inspect
module if you want to know how.
Multiple Exceptions¶
As seen above, you can catch multiple exceptions in an except
statement
If you want to do something completely different with each exception type, you can have multiple except
blocks:
try:
some_code
except IOError:
handle_the_error
except BufferError:
handle_the_error
except OSError:
handle_the_error
So a full-featured try
block has all of this:
try:
some_code
except IOError:
handle_the_error
except BufferError:
handle_the_error
...
else:
some code to run if none of these exceptions occurred
finally:
some code to run always.
The minimal try block is a try
, and one except
.
Raising Exceptions¶
def divide(a,b):
if b == 0:
raise ZeroDivisionError("b can not be zero")
else:
return a / b
(OK, this is a stupid example, as that error will be raised for you anyway. But bear with me).
When you call it:
In [515]: divide (12,0)
ZeroDivisionError: b can not be zero
Note how you can pass a message to the exception object constructor. It will get printed when the exception is printed. (and it is stored the in the Exception object’s .args
attribute)
Built in Exceptions¶
You can create your own custom exceptions.
But…
exp = \
[name for name in dir(__builtin__) if "Error" in name]
len(exp)
48
For the most part, you can/should use a built in one.
There are 48 built-in Exceptions – odds are good that there’s one that matches your use-case.
Also – custom exceptions require subclassing – and we haven’t learned that yet :-).
Choosing an Exception¶
Choose the best match you can for the built in Exception you raise.
Example:
if (not isinstance(m, int)) or (not isinstance(n, int)):
raise ValueError
Is the value of the input the problem here?
Nope: the type of the input is the problem:
if (not isinstance(m, int)) or (not isinstance(n, int)):
raise TypeError
but should you be checking type anyway? (EAFP)
What I usually do is run some code that’s similar that raises a built-in exception, and see what kind it raises, then I use that.
Knowing what Exception to catch¶
I usually figure out what exception to catch with an iterative process.
I write the code without a try block, pass in “bad data”, or somehow trigger the exception, then see what it is.
Example:
What if the file I want to read doesn’t exist?
In [7]: open("some_non_existant_file")
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
<ipython-input-7-a18e010ecdd0> in <module>()
----> 1 open("some_non_existant_file")
FileNotFoundError: [Errno 2] No such file or directory: 'some_non_existant_file'
Now I know to use:
except FileNotFoundError:
In the try
block where I am opening the file.