Python Exception Handling Techniques
Error reporting and processing through exceptions is one of Python’s key features. Care must be taken when handling exceptions to ensure proper application cleanup while maintaining useful error reporting.
Error reporting and processing through exceptions is one of Python’s key features. Unlike C, where the common way to report errors is through function return values that then have to be checked on every invocation, in Python a programmer can raise an exception at any point in a program. When the exception is raised, program execution is interrupted as the interpreter searches back up the stack to find a context with an exception handler. This search algorithm allows error handling to be organized cleanly in a central or high-level place within the program structure. Libraries may not need to do any exception handling at all, and simple scripts can frequently get away with wrapping a portion of the main program in an exception handler to print a nicely formatted error. Proper exception handling in more complicated situations can be a little tricky, though, especially in cases where the program has to clean up after itself as the exception propagates back up the stack.
Throwing and Catching
The statements used to deal with exceptions are raise
and except
. Both are language keywords. The most common form of throwing an exception with raise
uses an instance of an exception class.
1#!/usr/bin/env python
2
3def throws():
4 raise RuntimeError('this is the error message')
5
6def main():
7 throws()
8
9if __name__ == '__main__':
10 main()
The arguments needed by the exception class vary, but usually include a message string to explain the problem encountered.
If the exception is left unhandled, the default behavior is for the interpreter to print a full traceback and the error message included in the exception.
$ python throwing.py
Traceback (most recent call last):
File "throwing.py", line 10, in <module>
main()
File "throwing.py", line 7, in main
throws()
File "throwing.py", line 4, in throws
raise RuntimeError('this is the error message')
RuntimeError: this is the error message
For some scripts this behavior is sufficient, but it is nicer to catch the exception and print a more user-friendly version of the error.
1#!/usr/bin/env python
2
3import sys
4
5def throws():
6 raise RuntimeError('this is the error message')
7
8def main():
9 try:
10 throws()
11 return 0
12 except Exception, err:
13 sys.stderr.write('ERROR: %sn' % str(err))
14 return 1
15
16if __name__ == '__main__':
17 sys.exit(main())
In the example above, all exceptions derived from Exception
are caught, and just the error message is printed to stderr
. The program follows the Unix convention of returning an exit code indicating whether there was an error or not.
$ python catching.py
ERROR: this is the error message
Logging Exceptions
For daemons or other background processes, printing directly to stderr
may not be an option. The file descriptor might have been closed, or it may be redirected somewhere that errors are hard to find. A better option is to use the logging module to log the error, including the full traceback.
1#!/usr/bin/env python
2
3import logging
4import sys
5
6def throws():
7 raise RuntimeError('this is the error message')
8
9def main():
10 logging.basicConfig(level=logging.WARNING)
11 log = logging.getLogger('example')
12 try:
13 throws()
14 return 0
15 except Exception, err:
16 log.exception('Error from throws():')
17 return 1
18
19if __name__ == '__main__':
20 sys.exit(main())
In this example, the logger is configured to to use the default behavior of sending its output to stderr
, but that can easily be adjusted. Saving tracebacks to a log file can make it easier to debug problems that are otherwise hard to reproduce outside of a production environment.
$ python logging_errors.py
ERROR:example:Error from throws():
Traceback (most recent call last):
File "logging_errors.py", line 13, in main
throws()
File "logging_errors.py", line 7, in throws
raise RuntimeError('this is the error message')
RuntimeError: this is the error message
Cleaning Up and Re-raising
In many programs, simply reporting the error isn’t enough. If an error occurs part way through a lengthy process, you may need to undo some of the work already completed. For example, changes to a database may need to be rolled back or temporary files may need to be deleted. There are two ways to handle cleanup operations, using a finally
stanza coupled to the exception handler, or within an explicit exception handler that raises the exception after cleanup is done.
For cleanup operations that should always be performed, the simplest implementation is to use try:finally
. The finally
stanza is guaranteed to be run, even if the code inside the try
block raises an exception.
1#!/usr/bin/env python
2
3import sys
4
5def throws():
6 print 'Starting throws()'
7 raise RuntimeError('this is the error message')
8
9def main():
10 try:
11 try:
12 throws()
13 return 0
14 except Exception, err:
15 print 'Caught an exception'
16 return 1
17 finally:
18 print 'In finally block for cleanup'
19
20if __name__ == '__main__':
21 sys.exit(main())
This old-style example wraps a try:except
block with a try:finally
block to ensure that the cleanup code is called no matter what happens inside the main program.
$ python try_finally_oldstyle.py
Starting throws()
Caught an exception
In finally block for cleanup
While you may continue to see that style in older code, since Python 2.5 it has been possible to combine try:except
and try:finally
blocks into a single level. Since the newer style uses fewer levels of indentation and the resulting code is easier to read, it is being adopted quickly.
1#!/usr/bin/env python
2
3import sys
4
5def throws():
6 print 'Starting throws()'
7 raise RuntimeError('this is the error message')
8
9def main():
10 try:
11 throws()
12 return 0
13 except Exception, err:
14 print 'Caught an exception'
15 return 1
16 finally:
17 print 'In finally block for cleanup'
18
19if __name__ == '__main__':
20 sys.exit(main())
The resulting output is the same:
$ python try_finally.py
Starting throws()
Caught an exception
In finally block for cleanup
Re-raising Exceptions
Sometimes the cleanup action you need to take for an error is different than when an operation succeeds. For example, with a database you may need to rollback the transaction if there is an error but commit otherwise. In such cases, you will have to catch the exception and handle it. It may be necessary to catch the exception in an intermediate layer of your application to undo part of the processing, then throw it again to continue propagating the error handling.
1#!/usr/bin/env python
2"""Illustrate database transaction management using sqlite3.
3"""
4
5import logging
6import os
7import sqlite3
8import sys
9
10DB_NAME = 'mydb.sqlite'
11logging.basicConfig(level=logging.INFO)
12log = logging.getLogger('db_example')
13
14def throws():
15 raise RuntimeError('this is the error message')
16
17def create_tables(cursor):
18 log.info('Creating tables')
19 cursor.execute("create table module (name text, description text)")
20
21def insert_data(cursor):
22 for module, description in [('logging', 'error reporting and auditing'),
23 ('os', 'Operating system services'),
24 ('sqlite3', 'SQLite database access'),
25 ('sys', 'Runtime services'),
26 ]:
27 log.info('Inserting %s (%s)', module, description)
28 cursor.execute("insert into module values (?, ?)", (module, description))
29 return
30
31def do_database_work(do_create):
32 db = sqlite3.connect(DB_NAME)
33 try:
34 cursor = db.cursor()
35 if do_create:
36 create_tables(cursor)
37 insert_data(cursor)
38 throws()
39 except:
40 db.rollback()
41 log.error('Rolling back transaction')
42 raise
43 else:
44 log.info('Committing transaction')
45 db.commit()
46 return
47
48def main():
49 do_create = not os.path.exists(DB_NAME)
50 try:
51 do_database_work(do_create)
52 except Exception, err:
53 log.exception('Error while doing database work')
54 return 1
55 else:
56 return 0
57
58if __name__ == '__main__':
59 sys.exit(main())
This example uses a separate exception handler in do_database_work()
to undo the changes made in the database, then a global exception handler to report the error message.
$ python sqlite_error.py
INFO:db_example:Creating tables
INFO:db_example:Inserting logging (error reporting and auditing)
INFO:db_example:Inserting os (Operating system services)
INFO:db_example:Inserting sqlite3 (SQLite database access)
INFO:db_example:Inserting sys (Runtime services)
ERROR:db_example:Rolling back transaction
ERROR:db_example:Error while doing database work
Traceback (most recent call last):
File "sqlite_error.py", line 51, in main
do_database_work(do_create)
File "sqlite_error.py", line 38, in do_database_work
throws()
File "sqlite_error.py", line 15, in throws
raise RuntimeError('this is the error message')
RuntimeError: this is the error message
Preserving Tracebacks
Frequently the cleanup operation itself introduces another opportunity for an error condition in your program. This is especially the case when a system runs out of resources (memory, disk space, etc.). Exceptions raised from within an exception handler can mask the original error if they aren’t handled locally.
1#!/usr/bin/env python
2
3import sys
4import traceback
5
6def throws():
7 raise RuntimeError('error from throws')
8
9def nested():
10 try:
11 throws()
12 except:
13 cleanup()
14 raise
15
16def cleanup():
17 raise RuntimeError('error from cleanup')
18
19def main():
20 try:
21 nested()
22 return 0
23 except Exception, err:
24 traceback.print_exc()
25 return 1
26
27if __name__ == '__main__':
28 sys.exit(main())
When cleanup()
raises an exception while the original error is being processed, the exception handling machinery is reset to deal with the new error.
$ python masking_exceptions.py
Traceback (most recent call last):
File "masking_exceptions.py", line 21, in main
nested()
File "masking_exceptions.py", line 13, in nested
cleanup()
File "masking_exceptions.py", line 17, in cleanup
raise RuntimeError('error from cleanup')
RuntimeError: error from cleanup
Even catching the second exception does not guarantee that the original error message will be preserved.
1#!/usr/bin/env python
2
3import sys
4import traceback
5
6def throws():
7 raise RuntimeError('error from throws')
8
9def nested():
10 try:
11 throws()
12 except:
13 try:
14 cleanup()
15 except:
16 pass # ignore errors in cleanup
17 raise # we want to re-raise the original error
18
19def cleanup():
20 raise RuntimeError('error from cleanup')
21
22def main():
23 try:
24 nested()
25 return 0
26 except Exception, err:
27 traceback.print_exc()
28 return 1
29
30if __name__ == '__main__':
31 sys.exit(main())
Here, even though we have wrapped the cleanup()
call in an exception handler that ignores the exception, the error in cleanup()
hides the original error because only one exception context is maintained.
$ python masking_exceptions_catch.py
Traceback (most recent call last):
File "masking_exceptions_catch.py", line 24, in main
nested()
File "masking_exceptions_catch.py", line 14, in nested
cleanup()
File "masking_exceptions_catch.py", line 20, in cleanup
raise RuntimeError('error from cleanup')
RuntimeError: error from cleanup
A naive solution is to catch the original exception and retain it in a variable, then re-raise it explicitly.
1#!/usr/bin/env python
2
3import sys
4import traceback
5
6def throws():
7 raise RuntimeError('error from throws')
8
9def nested():
10 try:
11 throws()
12 except Exception, original_error:
13 try:
14 cleanup()
15 except:
16 pass # ignore errors in cleanup
17 raise original_error
18
19def cleanup():
20 raise RuntimeError('error from cleanup')
21
22def main():
23 try:
24 nested()
25 return 0
26 except Exception, err:
27 traceback.print_exc()
28 return 1
29
30if __name__ == '__main__':
31 sys.exit(main())
As you can see, this does not preserve the full traceback. The stack trace printed does not include the throws()
function at all, even though that is the original source of the error.
$ python masking_exceptions_reraise.py
Traceback (most recent call last):
File "masking_exceptions_reraise.py", line 24, in main
nested()
File "masking_exceptions_reraise.py", line 17, in nested
raise original_error
RuntimeError: error from throws
A better solution is to re-raise the original exception first, and handle the clean up in a try:finally
block.
1#!/usr/bin/env python
2
3import sys
4import traceback
5
6def throws():
7 raise RuntimeError('error from throws')
8
9def nested():
10 try:
11 throws()
12 except Exception, original_error:
13 try:
14 raise
15 finally:
16 try:
17 cleanup()
18 except:
19 pass # ignore errors in cleanup
20
21def cleanup():
22 raise RuntimeError('error from cleanup')
23
24def main():
25 try:
26 nested()
27 return 0
28 except Exception, err:
29 traceback.print_exc()
30 return 1
31
32if __name__ == '__main__':
33 sys.exit(main())
This construction prevents the original exception from being overwritten by the latter, and preserves the full stack in the traceback.
$ python masking_exceptions_finally.py
Traceback (most recent call last):
File "masking_exceptions_finally.py", line 26, in main
nested()
File "masking_exceptions_finally.py", line 11, in nested
throws()
File "masking_exceptions_finally.py", line 7, in throws
raise RuntimeError('error from throws')
RuntimeError: error from throws
The extra indention levels aren’t pretty, but it gives the output we want. The error reported is for the original exception, including the full stack trace.
See also
- Errors and Exceptions – The standard library documentation tutorial on handling errors and exceptions in your code.
- PyMOTW: exceptions – Python Module of the Week article about the exceptions module.
- exceptions module – Standard library documentation about the exceptions module.
- PyMOTW: logging – Python Module of the Week article about the logging module.
- logging module – Standard library documentation about the logging module.