Tips and Triks Error Handling in Phyton Script Tools
Tips and Triks Error Handling in Phyton Script Tools

I recently had a project that involved creating lots of Python script tools. Since other people were going to use these tools, I wanted my error handling to be robust and informative. This post is about some tips and tricks I discovered during this project.

Error handling basics

The basics of handling Python errors are covered in the 9.3 help topic Error handling with Python. The Python.org document Errors and Exceptions has more detailed information.

Tip #1 - Use the arcgisscripting.ExecuteError exception

In version 9.2, we introduced the arcgisscripting object along with a new exception class, arcgisscripting.ExecuteError. (This arcgisscripting.ExecuteError exception class wasn't documented in version 9.2, so few people knew about it.)† The arcgisscripting.ExecuteError exception is thrown whenever a geoprocessing tool or geoprocessing function encounters an error. What this means is that you can divide errors into two groups, geoprocessing errors (those that throw the arcgisscripting.ExecuteError exception) and everything else.† You can then handle the errors differently, as demonstrated in the code below:

import arcgisscripting
gp = arcgisscripting.create(9.3)
try:
result = gp.getcount("C:/blah.shp")
# x = y

# Return GEOPROCESSING specific errors
#

except arcgisscripting.ExecuteError:
gp.AddError(gp.GetMessages(2))

# Return any PYTHON or system specific errors
#

except:
gp.AddError("Python or system error occurred")

The code above is used as the source for a script tool. To keep things simple, the script tool has no parameters. When the script tool is executed, the call to getcount produces an error because the dataset "C:/blah.shp" doesn't exist. As a result, the progress dialog looks as follows:

To prove how geoprocessing errors and Python errors are handled differently, change the two lines of code as follows:

    # result = gp.getcount("C:/blah.shp")
x = y

Now when the script is executed, an error will occur because the variable 'y' is undefined, and the progress dialog will display as follows:

Tip #2 - Beware of getting error messages from a result object

Before moving on, a quick word about the result object, shown below:

    result = gp.getcount("C:/blah.shp")

If the call to getcount above raises an exception, the result object is null. This means you cannot retrieve error messages from the result object. For example:

import arcgisscripting
gp = arcgisscripting.create(9.3)
try:
result = gp.getcount("C:/blah.shp")

# Return GEOPROCESSING specific errors
# (this method is INCORRECT!)

except arcgisscripting.ExecuteError:
gp.AddError(result.GetMessages(2))

# Return any PYTHON or system specific errors
#

except:
gp.AddError("Python or system error occurred")

The above code will fail with the message "name 'result' is not defined". This is due to the fact that the result object is null. Since the result object is null, a Python error will be raised since a null object doesn't have a GetMessages() method. Note -- a result object created by calling a geoprocessing service on an ArcGIS Server is never null. Null result objects a created only when a tool is run locally and it raises an error.† For more information about using the result object, see Getting results from a geoprocessing tool.

Tip #3 - Use AddReturnMessage() to preserve links to error codes

In version 9.3, geoprocessing error numbers shown in the progress dialog are hyperlinks to a help page that further describes the error. To enable hyperlinks for errors in your script, you need to use the AddReturnMessage() method instead of the AddError method, as follows:
import arcgisscripting
gp = arcgisscripting.create(9.3)
try:
result = gp.getcount("C:\\blah.shp")

# Return GEOPROCESSING specific errors
#

except arcgisscripting.ExecuteError:
for msg in range(0, gp.MessageCount):
if gp.GetSeverity(msg) == 2:
gp.AddReturnMessage(msg)


# Return any PYTHON or system specific errors
#

except:
gp.AddError("Python or system error occurred")

Now when the script is executed, the progress dialog will look as follows:


Tip #4 -- Use traceback to return more information about the error

The help system topic Error handling with Python shows you how to use Python's traceback to retrieve the line number causing the error, the name of the .py file, and, for Python errors, a description of the error.

At version 9.3, you can run script tools in-process, as described in Running a script in process. Scripts run in-process execute much faster than scripts run out-of-process, so you always want to run the script in-process. When a script is run in-process, ArcGIS reads the .py file into memory and then executes it. Because ArcGIS puts the .py files into memory, the Python traceback module doesn't know the name of the .py file and will report the file name as "<string>". If you want to report the name of the .py file causing the error (very useful for debugging), you'll have to provide it yourself.

The script below has a local routine, trace(), that uses traceback to return the line number, the file name, and the error description. The routine also contains the the hard-coded file name (shown in bold) which you must change for your script.

import arcgisscripting
gp = arcgisscripting.create(9.3)
try:
def trace():
import os, sys, traceback
tb = sys.exc_info()[2]
tbinfo = traceback.format_tb(tb)[0] # script name + line number
line = tbinfo.split(", ")[1]

# Construct the pathname to this script. Get the pathname
# to the folder containing the script using sys.path[0].
# Modify line below and replace 'myscript.py' with the name of
# this script.
#

filename = sys.path[0] + os.sep + "myscript.py"

# Get Python syntax error
#

synerror = traceback.format_exc().splitlines()[-1]
return line, filename, synerror
if __name__ == '__main__':
# y = x
result = gp.getcount("C:/blah.shp")

except arcgisscripting.ExecuteError:
# Call our trace routine to retrieve line number and filename.
# (returned variable 'err' is ignored since this is a gp error.
#

line, filename, err = trace()
gp.AddError("Geoprocessing error on " + line + " of " + filename + " :")
for msg in range(0, gp.MessageCount):
if gp.GetSeverity(msg) == 2:
gp.AddReturnMessage(msg)

except:
line, filename, err = trace()
gp.AddError("Python error on " + line + " of " + filename)
gp.AddError(err)

Below are illustrations of the progress dialog showing the line number and file name for both a geoprocessing error and a Python syntax error (by commenting out the "y = x" statement).

Tip #5 -- Use Finally statement to clean up cursors

Something I discovered by reading the Python doc Errors and Exceptions is the 'finally' statement. No matter what happens in your Python script, code in a 'finally:' block always gets executed. It's a great place to put clean-up actions, such as deleting cursors, as shown in the code below.

import arcgisscripting
gp = arcgisscripting.create(9.3)

def trace():
import os, sys, traceback
tb = sys.exc_info()[2]
tbinfo = traceback.format_tb(tb)[0]
line = tbinfo.split(", ")[1]
filename = sys.path[0] + os.sep + "testscript.py"
synerror = traceback.format_exc().splitlines()[-1]
return line, filename, synerror

if __name__ == '__main__':
# Define variables for a cursor and its row
#

cursor, row = None, None
try:
cursor = gp.searchcursor("E:/Data/CityOfSanFrancisco.gdb/FireStations")
row = cursor.Next()
while row:
# Code here...
# for demonstration...this next statement will cause an error

y = x
row = cursor.Next()
except arcgisscripting.ExecuteError:
line, filename, err = trace()
gp.AddError("Geoprocessing error on " + line + " of " + filename + " :")
for msg in range(0, gp.MessageCount):
if gp.GetSeverity(msg) == 2:
gp.AddReturnMessage(msg)
except:
line, filename, err = trace()
gp.AddError("Python error on " + line + " of " + filename)
gp.AddError(err)
finally:
# Close cursor and row objects.
#

del row, cursor

# Print a message for demo purposes
#

gp.AddMessage("\n ** Cursor and row have been deleted ** \n")

As illustrated below, the cursor gets deleted even if there is an error in the script.


In the main body of the script, the cursor and row object variables are declared and initialized to None (shown in bold in the above code).† This ensures that these two variables exist when the del statement in the finally clause is executed; if you try to delete a non-existent variable, an exception will be thrown.††

About cursors and the del statement

The Python del statement deletes a variable. You can use it to delete any Python variable, such as a list or dictionary. However, you rarely need to delete native Python variables.† The del statement is mainly used for deleting non-native variables, such as geoprocessing cursors and rows obtained from the cursor. When you delete a cursor and its row, all pending changes to the row (caused by an update or insert cursor) are flushed and all locks on the dataset are removed.† You should always delete cursors and rows obtained from the cursor.† When deleting a cursor, delete the row object first, then the cursor.

Source: http://blogs.esri.com