Exceptions¶
Indices and tables¶
There are many situations where the normal flow of your program needs to be interrupted to deal with an unusual or exceptional condition. Quite often, you can’t predict when (in time) or where (in your code) these exceptional conditions will occur. For example:
- The user presses
CTRL-C
on the keyboard- The disk you are trying to write to becomes full
- Your program attempted to divide something by 0.
Python pre-defines a lot of exceptions, which can be found here. When one of these exceptional conditions is detected, Python raises an exception at the point at which it detects the problem. It is also possible for you to purposely raise exceptions from within your code.
Exceptions are not automatically fatal as Python provides syntax to trap for and handle them.
The try Statement¶
The try
statement is used to setup exception handling around a group of statements that you want to protect.
It looks generally like the following [1]:
try:
try_suite
except exception_group1 as variable1:
except_suite1
...
except exception_groupN as variableN:
except_suiteN
else:
else_suite
finally:
finally_suite
Let’s break down the various parts:
The try
and try_suite
¶
Required: Yes
try: try_suite except exception_group1 as variable1: except_suite1 ... except exception_groupN as variableN: except_suiteN else: else_suite finally: finally_suite
- This is where the protected statements are placed. If an exception occurs while this suite of code is being executed, the rest of the statements in the suite are skipped and Python searches for an exception handler to handle it.
The except
, variable
and except_suite
¶
Required: except
and except_suite
-> Yes (1 or more); variable
-> no
try: try_suite except exception_group1 as variable1: except_suite1 ... except exception_groupN as variableN: except_suiteN else: else_suite finally: finally_suite
This is where you specify the class name of the exception(s) to trap for and the handling code to execute for each. Can be:
- A single exception, or,
- A tuple of exceptions, or,
- Empty, which traps for all exceptions (for obvious reasons, be careful to put this as the last except clause).
For example:
except KeyboardInterrupt as ex: pass except (OSError, EOFError) as ex: pass except: # Put empty 'except' clause last # as it traps for everything else pass
- When an exception is thrown, the
except
statements are searched in-turn until a matching one is found.- Exceptions that are thrown will match an
except
clause if the exception given in theexcept
clause is the same class as, or base class of, the exception that was thrown.- If the
as
keyword is specified, the exception object is made available to the handler by the nameexcept
.- Once an
except
clause is matched, the search is terminated. If no matchingexcept
clause is found, the search continues in the surrounding code blocks.Note
If no exception handler is found by the time the top of the program is reached, this is considered an unhandled exception and the program is terminated. When the program is terminated this way, a stack trace is printed to the screen to assist with debug. This is often viewed as “dirty” by users of your program because it is not only cryptic and difficult for most people to read and interpret, but, it also exposes a hole in your software you didn’t protect against.
- If the exception is handled, program execution continues into the
finally
block (if present) and then off the end of thetry
statement.- If the exception handler causes another exception, execution of the
except_suite
is stopped, the original exception is nested inside the new exception via the new exception’s__context__
attribute and a search is started fo find a handler for the new exception.Tip
Generally speaking, don’t trap for exceptions unless you intend to do something about it. Trapping it but doing nothing is useless. Trapping it and re-throwing it is useless.
The else
and else_suite
¶
Required: No
try: try_suite except exception_group1 as variable1: except_suite1 ... except exception_groupN as variableN: except_suiteN else: else_suite finally: finally_suite
- If present, the
else_suite
is executed if no exception occurs. Otherwise, it is skipped.- If an exception occurs in the
else
clause, the precedingexcept
clauses are not searched again. The exception must be handled by an outer exception trap.
The finally
and finally_suite
¶
Required: No
try: try_suite except exception_group1 as variable1: except_suite1 ... except exception_groupN as variableN: except_suiteN else: else_suite finally: finally_suite
- Is always executed, whether an exception happens or not. If there is no exception handler in the try structure to handle the current exception, the finally block is still executed and the exception subsequently re-raised.
Tip
Use a finally block to allow the program to do things like clean up after itself (for example, release file handles and close network sockets gracefully).
- If you use a
break
,continue
orreturn
statement in a try suite, the finally clause is still executed. A return statement is therefore useless in the try suite if a return value is also present in the finally suite because the value returned is always from the last return statement executed. For example:>>> def foo(): ... try: ... return 'Hi' ... except Exception as ex: ... pass ... finally: ... return "Bye" >>> foo() 'Bye'
- If the
finally_suite
causes another exception, execution of thefinally_suite
is stopped, the original exception is nested inside the new exception via the new exception’s__context__
attribute and a search is started fo find a handler for the new exception.
Exception Arguments¶
Exceptions have “associated values” (a.k.a. the “exception arguments”) which frequently contains information about the cause of the exception. Most exceptions take at least one, optional, input object (like a number or a string as a message). Exactly what information is provided is exception dependent. The description of the built-in exception classes talks about what extra information is available in them.
If the interpreter creates the exception, it populates the associated values.
If your program purposely raises an exception, you can set those values when you call the exception objects constructor to create it.
If you try and print an exception object, the exceptions associated values is typically what is printed.
For example the following code:
fh = None
try:
fh = open('asdf.txt')
except OSError as ex:
print("Oh no...something went wrong. Here's what it is:")
print(ex)
finally:
if fh is not None:
fh.close()
Returns the following when printing the exception object:
Oh no...something went wrong. Here's what it is:
[Errno 2] No such file or directory: 'asdf.txt'
Raising Exceptions¶
Your program can throw an exception by executing the raise
statement, which has the following general syntax:
-
raise exception(*args)
Raises the given
exception
, which can be either an exception class name, or, an instance of an exception class.
For example:
def func():
print("FUNC: Doing some stuff...")
print("FUNC: *** Raising an exception ***")
raise AttributeError("Do you have the right duck?")
print("FUNC: More stuff...")
try:
print("TRY : Executing try...")
func()
except AttributeError as ex:
print("EXC : Got an exception. Handling it.")
print("EXC : {}".format(type(ex)))
print("EXC : {}".format(ex))
finally:
print("FIN : Done try")
Produces the following output:
TRY : Executing try...
FUNC: Doing some stuff...
FUNC: *** Raising an exception ***
EXC : Got an exception. Handling it.
EXC : <class 'AttributeError'>
EXC : Do you have the right duck?
FIN : Done try
If you truly want to just re-raise an exception that you trapped, you can just type raise
.
def func():
print("FUNC: Doing some stuff...")
print("FUNC: *** Raising an exception ***")
raise AttributeError("Do you have the right duck?")
print("FUNC: More stuff...")
try:
print("OTRY: Executing outer try...")
# NOTE: Nested try block
try:
print("ITRY: Executing inner try...")
func()
except Exception as ex:
print("IEXC: Got an exception, but, can't handle it.")
print("IEXC: Re-raising...")
raise
finally:
print("IFIN: Done inner try")
except AttributeError as ex:
print("OEXC: Got an exception. Handling it.")
print("OEXC: {}".format(type(ex)))
print("OEXC: {}".format(ex))
finally:
print("OFIN: Done outer try")
Produces the following output:
OTRY: Executing outer try...
ITRY: Executing inner try...
FUNC: Doing some stuff...
FUNC: *** Raising an exception ***
IEXC: Got an exception, but, can't handle it.
IEXC: Re-raising...
IFIN: Done inner try
OEXC: Got an exception. Handling it.
OEXC: <class 'AttributeError'>
OEXC: Do you have the right duck?
OFIN: Done outer try
Custom Exception Classes¶
Exceptions are objects and new object classes are created by deriving from a base class. In Python, new exceptions are created by deriving from the Exception class, or, one of its sub-classes. The hierarchy of built-in exceptions is found here.
Tip
When creating your own exception class the name should usually end in ‘Error’ (unless of course it actually doesn’t represent an error).
For example a new exception can be defined as simply as:
>>> class MyException(Exception): pass
And then you can use it in your program:
def func():
print("FUNC: Doing some stuff...")
print("FUNC: *** Raising an exception ***")
raise MyException("So do you like, stuff?")
print("FUNC: More stuff...")
try:
print("TRY : Executing try...")
func()
except MyException as ex:
print("EXC : Got an exception. Handling it.")
print("EXC : {}".format(type(ex)))
print("EXC : {}".format(ex))
finally:
print("FIN : Done try")
TRY : Executing try...
FUNC: Doing some stuff...
FUNC: *** Raising an exception ***
EXC : Got an exception. Handling it.
EXC : <class '__main__.MyException'>
EXC : So do you like, stuff?
FIN : Done try
Exception classes can do what any other class can do (execute specific code on construction, define additional properties etc). However, that will be covered in the section on Classes.
Performance Advantages¶
A common coding style in Python embraces the notion that it is easier to ask for forgiveness than permission. The try
statement supports this style.
Consider for example, the two approaches to opening a file:
- Ask for permission: Check if the file exists and that you have permissions to read it prior to trying to open the file. Implies use of the
if
statement.- Beg for forgiveness: Try to open the file, if there is an exception generated from it being a non-existent file or insufficient user permissions, handle it gracefully. Implies use of the
try
statement.
We can test the performance difference between these two styles using the code contained in this notebook
which declares two life form classes, and asks them to make their characteristic sound if they can.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class duck(object):
# Can make a sound
def make_sound(self):
print("QUACK!")
class fish(object):
# Can't make a sound
pass
def ask_permission(obj):
if hasattr(obj, "make_sound"):
obj.make_sound()
else:
print("Life form makes no sound.")
def beg_forgiveness(obj):
try:
obj.make_sound()
except AttributeError:
print("Life form makes no sound.")
|
The performance results from running this code 1000x, and averagaed over multple runs, are found here
and summarized in Table 16:
Style | Makes Sound (Duck) | Sound-less (Fish) |
---|---|---|
Ask Permission (IF) | 0.002834s | 0.003065s |
Beg Forgiveness (TRY) | 0.002676s | 0.003300s |
Difference | -0.0001575s | 0.0002355s |
Winner | BEG (IF) | ASK (TRY) |
The performance take-aways from this table are the following:
- The “IF” version always costs you. The more costly the check (for example checking a network drive vs checking a local drive), the greater the performance penalty compared to the “TRY” version.
- The “TRY” version only costs you if an exception is actually thrown (and then it can be quite costly).
So, if the chance of an exception is high, then use the “IF” version. But if the exception is infrequent (i.e. truly exceptional), use the “TRY” version.
From a code construction point of view (applies to all applications) the “TRY” version is generally favored overall because:
- The “IF” version has you wind up with a lot of “if error” in your code which can affect readability and maintainability.
- The “TRY” version cleanly separates the business logic from the error logic.
General suggestions:
- By default wrap your code with TRY statements to improve readability and maintainability.
- If you have performance issues, investigate why and selectively convert to the “IF” based structure as warranted.
[1] | Syntax representation re-used from, Programming in Python3, Mark Summerfield, 2009, Addison-Wesley. |