Writing messages in script tools

In your scripts, you need to import ArcPy, as follows:

import arcpy

When the script is run as a script tool, ArcPy is fully aware of the application it is called from, such as ArcMap or ArcCatalog. It is, in fact, using the same objects the application is using rather than creating new ones. All environment settings made in the application, such as overwriteOutput and scratchWorkspace, are available. One major effect of this is that you can write messages with ArcPy and your messages automatically appear on the progress dialog box, the tool's result, and the Python window. It also means that any model or script tool that calls your script tool has access to the messages you write.

Contrast this to a stand-alone script. In a stand-alone script, your script isn't being called from an ArcGIS application, so there is no progress dialog box, result, or Python window where your messages can be viewed. If you call another (stand-alone) script from within your script, the called script (if it uses geoprocessing tools) is completely separate, distinct from the one you created, and there is no sharing of messages between them. One of the main advantages of script tools is the sharing of messages and environments.

The four ArcPy functions for writing messages are as follows:

Example of adding messages

The next example copies a list of feature classes from one workspace to another. An automatic conversion takes place if the workspaces are of a different type, such as a geodatabase to a folder. Error handling is used to catch any problems and return messages; otherwise, informative messages of success are returned during execution. This example makes use of the from <module> import directive. In this case, the module is ScriptUtils, and its code is also found below. This code also uses try/except blocks.

Learn more about try/except blocks

# ConvertFeatures.py 
#   Converts feature classes by copying them from one workspace to another
#
# Import the ScriptUtils utility module which, in turn,  imports the 
#  standard library modules and imports arcpy
#
from ScriptUtils import *

# Get the list of feature classes to be copied to the output workspace
#
in_feature_classes = arcpy.GetParameterAsText(0)

# Establish an array of input feature classes using the ScriptUtils routine.
#
in_feature_classes = SplitMulti(in_feature_classes)

# Get the output workspace
#
out_folder = arcpy.GetParameterAsText(1)

# Loop through the array copying each feature class to the output workspace
#
for in_feature_class in in_feature_classes:
    try:
        # Create the output name
        #
        feature_class_name = arcpy.ValidateTableName(in_feature_class, out_folder)

        # Add an output message
        #
        arcpy.AddMessage("Converting: " + in_feature_class + " To " +\
                         feature_class_name + ".shp")
    
        # Copy the feature class to the output workspace using the ScriptUtils routine
        #
        CopyFeatures(in_feature_class, out_folder + os.sep + \
                     feature_class_name)

        # If successful, add another message
        #
        arcpy.AddMessage("Successfully converted: " + in_feature_class + \
                          " To " + out_folder)
        
    except StandardError, ErrDesc:
        arcpy.AddWarning("Failed to convert: " + in_feature_class)
        arcpy.AddWarning(ErrDesc)
    except:
        arcpy.AddWarning("Failed to convert: " + in_feature_class)
        if not arcpy.GetMessages(2) == "":
            arcpy.AddError(arcpy.GetMessages(2))

Here is the ScriptUtils module used above.

# ScriptUtils.py

# Import required modules
#
import arcpy
import sys
import string
import os

def SplitMulti(multi_input):
    try:
        # Split the multivalue on the semi-colon delimiter
        #
        multi_as_list = string.split(multi_input, ";")
        return multi_as_list
    except:
        ErrDesc = "Error: Failed in parsing the inputs."
        raise StandardError, ErrDesc
        
def CopyFeatures(in_table, out_table):
    try:
        ErrDesc = "CopyFeatures failed"

        # Copy each feature class to the output workspace
        #
        arcpy.CopyFeatures_management(in_table, out_table)
    except:
        if arcpy.GetMessages(2) != "":
            ErrDesc = arcpy.GetMessages(2)
        raise StandardError, ErrDesc

Notes about the above scripts

Functions, modules, and objects can be imported from other Python scripts to simplify your script and centralize code. The import statement is used to pull everything from another script, or just the entities you want.

Exceptions are events that may modify the flow of control through a program. They may be triggered or intercepted within a script using the try and raise statements. StandardError is a built-in exception on which most exception types are based in Python. It is called if a tool error occurs with the ErrDesc variable set by the tool's error message.

Learn more about error handling in Python

Strings, defined in the string module, are a built-in type used to store and represent text. A number of operations are supported for manipulating strings, such as concatenation, slicing, and indexing.

The operating system (os) module provides a generic interface to the operating system's basic set of tools.

Returning all messages from a tool

There are times when you may want to return all messages from a tool you've called, regardless of message severity. Using an index parameter, the AddReturnMessage function returns a message from ArcPy's message array. The example below shows how to return all a tool's messages:

arcpy.Clip_analysis("roads","urban_area","urban_roads")
# Return the resulting messages as script tool output messages
#
x = 0
while x < arcpy.MessageCount:
    arcpy.AddReturnMessage(x)
    x = x + 1

Messaging in a dual-purpose script

You can design your script for dual purposes: It can be used both as a stand-alone script (from the operating system) or as a script tool. There are three considerations:

Standard practice is to write an error-reporting routine that writes messages to both standard output (using print) and using ArcPy (using AddMessage, AddWarning, and AddError functions). Here is such a routine, taken from the Multiple Ring Buffer tool, a script tool in the Analysis toolbox. You can view the Multiple Ring Buffer script by locating the tool in Analysis Toolbox, right-clicking, then clicking Edit.

def AddMsgAndPrint(msg, severity=0):
    # Adds a Message (in case this is run as a tool)
    # and also prints the message to the screen (standard output)
    # 
    print msg

    # Split the message on \n first, so that if it's multiple lines, 
    #  a GPMessage will be added for each line
    try:
        for string in msg.split('\n'):
            # Add appropriate geoprocessing message 
            #
            if severity == 0:
                arcpy.AddMessage(string)
            elif severity == 1:
                arcpy.AddWarning(string)
            elif severity == 2:
                arcpy.AddError(string)
    except:
        pass

Controlling the progress dialog box

Since script tools share the application, you have control of the progress dialog box. You can control the appearance of the progress dialog box by choosing either the default progressor or the step progressor, as illustrated below.

Default and step progressor

There are four functions you use to control the progress dialog box and its progressor.

Function

Description

SetProgressor

Sets the type of progressor (default or step); its label; and the minimum, maximum, and interval for step progressors

ResetProgressor

Resets the progressor

SetProgressorPosition

Moves the step progressor by an increment

SetProgressorLabel

Changes the label of the progressor

Geoprocessing progressor functions

The code below demonstrates full use of the default and step progressor. Copy this code into your Python editor, save it, then create a script tool for it. The script tool has two input Long parameters as described in the code comments. Then run the script tool, providing different values for the parameters (start with n = 10 and p = 1, then try n = 101 and p = 3).

# Demonstration script showing examples of using the progressor
#  Parameters:
#   n - number to count to (a good first choice is 10)
#   p - interval to count by (a good first choice is 1)
# The various time.sleep() calls are just to slow the dialog down
#  so you can view messages and progressor labels.
#
import arcpy
import time

n = int(arcpy.GetParameterAsText(0))
p = int(arcpy.GetParameterAsText(1))

readTime = 2.5 # Pause to read what's written on dialog
loopTime = 0.3 # Loop iteration delay

arcpy.AddMessage("Running demo with: " + str(n) + " by " + str(p))

# Start by showing the default progress dialog, where the
#  progress bar goes back and forth. Note how the progress label
#  mimics working through some "phases", or chunks of work that
#  a script may perform.
#
arcpy.SetProgressor("default", "This is the default progressor")
time.sleep(readTime)

for i in range(3):
  arcpy.SetProgressorLabel("Working on \"phase\" " + str(i + 1))
  arcpy.AddMessage("Messages for phase " + str(i+1))
  time.sleep(readTime)
arcpy.AddMessage("-------------------------")

# Setup the progressor with its initial label, min, max, and interval
#
arcpy.SetProgressor("step", "Step progressor: Counting from 0 to " + str(n), 0, n, p)
time.sleep(readTime)

# Loop issuing a new label when the increment is divisible by the
#  value of countBy (p). The "%" is python's modulus operator - we
#  only update the position every p'th iteration
#
for i in range(n):
  if (i % p) == 0:
    arcpy.SetProgressorLabel("Iteration: " + str(i))
    arcpy.SetProgressorPosition(i)
    time.sleep(loopTime)

# Update the remainder that may be left over due to modulus operation
#  
arcpy.SetProgressorLabel("Iteration: " + str(i+1))
arcpy.SetProgressorPosition(i+1)

arcpy.AddMessage("Done counting up")
arcpy.AddMessage("-------------------------")
time.sleep(readTime)

# Just for fun, make the progressor go backwards.
#
arcpy.SetProgressor("default", "Default progressor: Now we'll do a countdown")
time.sleep(readTime)
arcpy.AddMessage("Here comes the countdown...")
arcpy.SetProgressor("step", "Step progressor: Counting backwards from " + str(n), 0, n, p)
time.sleep(readTime)
arcpy.AddMessage("Counting down now...")

for i in range(n, 0, -1):
  if (i % p) == 0:
    arcpy.SetProgressorLabel("Iteration: " + str(i))
    arcpy.SetProgressorPosition(i)
    time.sleep(loopTime)

# Update for remainder
#
arcpy.SetProgressorLabel("Iteration: " + str(i-1))
arcpy.SetProgressorPosition(i-1)
time.sleep(readTime)    
arcpy.AddMessage("-------------------------")
arcpy.AddMessage("All done")
arcpy.ResetProgressor()

Choosing a good increment when maximum is potentially large

It's not at all uncommon to write scripts that will iterate an unknown number of times. For example, your script may use a SearchCursor to iterate over all rows in a table, and you don't know the number of rows beforehand—your script may be used with tables of any size, from a few thousand rows to millions of rows. Incrementing a step progressor for every row in a large table is a performance bottleneck, and you may want to guard against such performance bottlenecks.

To demonstrate and assess performance issues with step progressors, copy the code below into your Python editor, save it, then create a script tool for it. The tool has two inputs: a table parameter and a field parameter. Run the script tool with a variety of table sizes, but be sure to try a table or feature class containing 10,000 or more rows to see performance differences. (You can also try running the tool in- and out-of-process to see the performance boost running in-process.)

The script executes three separate loops, and each loop fetches all the rows in the table. The loops differ in how they update the step progressor. The first and second loops update the step progressor in large increments, and the last loop increments the step progressor once for each row. When you run the tool, you see that this last loop takes longer to run.

You may want to employ the techniques found in this code for your script tools.

# Demonstrates a step progressor by looping through records
#  on a table. Use a table with 10,000 or so rows - smaller tables
#  just whiz by.
#   1 = table name
#   2 = field on the table

import arcpy

try:
  inTable = arcpy.GetParameterAsText(0)
  inField = arcpy.GetParameterAsText(1)

  # Determine n, number of records on the table
  #
  arcpy.AddMessage("Getting row count")
  n = arcpy.GetCount_management(inTable)
  if n == 0:
    raise "no records"
  
  arcpy.AddMessage("Number of rows = " + str(n))
  arcpy.AddMessage("")
  arcpy.AddMessage("---------------------------------")
  
  # Method 1: Calculate and use a suitable base 10 increment 
  # ===================================
  import math
  p = int(math.log10(n))
  if not p:
    p = 1
  increment = int(math.pow(10, p-1))

  arcpy.SetProgressor("step", "Incrementing by " + str(increment) + " on " + \
                      inTable, 0, n, increment)

  rows = arcpy.SearchCursor(inTable)
  i = 0
  beginTime = time.clock()
  for row in rows:
    if (i % increment) == 0:
      arcpy.SetProgressorPosition(i)
    fieldValue = row.getValue(inField)
    i = i + 1

  arcpy.SetProgressorPosition(i)
  arcpy.AddMessage("Method 1")
  arcpy.AddMessage("Increment = " + str(increment))
  arcpy.AddMessage("Elapsed time: " + str(time.clock() - beginTime))
  arcpy.AddMessage("---------------------------------")
  
  del rows
  del row

  # Method 2: let's just move in 10 percent increments
  # ===================================
  increment = int(n/10.0)
  arcpy.SetProgressor("step", "Incrementing by " + str(increment) + " on " + inTable, \
                   0, n, increment)
  
  rows = arcpy.SearchCursor(inTable)
  i = 0
  beginTime = time.clock()
  for row in rows:
    if (i % increment) == 0:
      arcpy.SetProgressorPosition(i)
    fieldValue = row.getValue(inField)
    i = i + 1
    
  arcpy.SetProgressorPosition(i)
  arcpy.AddMessage("Method 2")
  arcpy.AddMessage("Increment = " + str(increment))
  arcpy.AddMessage("Elapsed time: " + str(time.clock() - beginTime))
  arcpy.AddMessage("---------------------------------")
  
  del rows
  del row

  # Method 3: use increment of 1
  # ===================================
  increment = 1
  arcpy.SetProgressor("step", "Incrementing by 1 on " + inTable, 0, n, increment)
  
  rows = arcpy.SearchCursor(inTable)

  beginTime = time.clock()
  while row:
    arcpy.SetProgressorPosition()
    fieldValue = row.getValue(inField)

  arcpy.SetProgressorPosition(n)
  arcpy.ResetProgressor()
  arcpy.AddMessage("Method 3")
  arcpy.AddMessage("Increment = " + str(increment))
  arcpy.AddMessage("Elasped time: " + str(time.clock() - beginTime))
  arcpy.AddMessage("---------------------------------")
  arcpy.AddMessage("")
  arcpy.AddMessage("Pausing for a moment to allow viewing...")
  time.sleep(2.0) # Allow viewing of the finished progressor

  del rows
  del row

except "no records":
  arcpy.AddWarning(inTable + " has no records to count")

except:
  if rows:
    del rows
  if row:
    del row
  arcpy.AddError("Exception occurred")

4/14/2011