Creating a Python Script Tool in ArcGIS Pro

This tutorial is going to cover making a Python Script Tool Toolbox in ArcGIS Pro. I made a lot of python script tools in ArcGIS Desktop while completing my Advanced Diploma in GIS at the Centre of Geographic Sciences (COGS) as well as for my own projects. This post is going to outline how to create the script tool in ArcGIS Pro. I will try to highlight any major differences I notice and any problems I run into. Overall the process is similar, but there are some key differences.

I am going to be making a relatively simple tool to keep the tutorial simple and straight forward. I wanted to highlight a few different features of script tools that I found useful as well. The tool I am making will take an input feature class of points, create a hexbin grid which covers the extent of the points and then count the number of points in each hexbin grid. Now you are probably thinking to yourself ‘Ryan, you really like hexbin grids, is that all you ever post about?’ Well it is true that my love for them is stronger than most, they are super useful and the maps look great, so why not. The process I am using is very similar to my previous post however this time we are turning it into a python script tool. This means that the workflow is automated – after the inputs are set up all you have to do is press Run. Another of my true loves is automation, so this script tool fits right in.

Before we dive into the procedure here is a sneak preview of the input I am using and the output. The input point file makes it really hard to see just how many points there are due to over plotting. Once they are on top of each other its really any ones guess as to how many points there are. Look at that shiny hexbin map on the right, now it is clear how many points are in each bin. I think it looks fantastic! Now onto the procedure for creating our python script tool to automate this workflow.

 

Input point map

Input point map

Output hexbin map

Output hexbin map

Getting Started

To get started I created a new project in ArcGIS Pro. Then in the Project Pane (this pane seems to have all of the features that I am used to seeing in ArcCatalog) navigate to the Toolboxes folder. There should be a Toolbox already created for your project, but if not right click on Toolboxes and select New Toolbox. Then right click on the new or existing Toolbox and select New>Script

Project Pane - New Script

This will load the script tool dialogue box. In it you need to type in a Name (actual name of the script, no spaces or special characters allowed), a Label (you can use spaces here, this is what will show up in the Toolbox) and pick your script file. I just created an empty python script in my project folder and chose that. We will be writing the code for the python script as we go.

Create Script Tool - General

Python Script Tool Parameters

The next step is to setup the script tool parameters. The parameters are the inputs that a user fills in with when setting up their run of the tool. Here you will be able to select items like input feature classes, output feature classes, input text values and fields from input feature classes. Below is an image of the parameters I selected, I will go through each parameter in detail.

Script Tool Parameters

The first couple of columns are the most commonly used. The Label is what the user will see when they are filling in the script tool. Name is a reference name for the row, it cannot have spaces or special characters. Data Type is the type of data you will be using. There are a lot of options here. Some of the more common ones are Feature Class (this is a feature Class), Field (these are input field names), String (just a regular string), and Long (a numerical input). Type has 3 options, Required – the user must fill in the value, Optional – the user could fill in the value and Derived – the value is derived from another field. Direction lets you specify whether the value is an input (i.e., it must already exist) or output (i.e., it will be created by the tool). I did not end up using Category. Filter lets you specify a filter for the values that the field allows. Dependency lets you set up the input so it depends on another one of the input values. Default lets you specify the default value, it is useful for settings that are generally the same, but the user could change if they wanted. 

The ‘Input Feature Class‘ allows the user to select a feature class. It is required and set as an input so the feature class must already exist. The ‘Output Feature Class‘ is also required but it is an output. This allows the user to select the name of the output the tool will generate. The ‘Input Field‘ is optional and the main reason I put it here is for demonstration purposes. It is of the type field and it is set to have a dependency on the Input Feature Class. This makes it so this field is populated with the fields contained in the Input Feature Class. This is a great option so you can let the user specify a field or fields and use those in your tool. It only gets populated with fields that exist so your user won’t have to type the value in. The last field is ‘Hexbin Size’, and this one I set the data type to Long so it expects a numerical input. On this field I also set up a filter. A filter on a numerical input value allows you to specify either a range or a list of values. I chose range and made it so the value had to be between 1 and 50. This way I am sure to be getting a number for the input within a certain range. 

Range Filter

Setting up a Range Filter in ArcGIS Pro looks very different compared to ArcGIS Desktop. When running the tool in ArcGIS Desktop this type of input would produce a slider line and let you chose between the minimum and maximum values. In ArcGIS Pro you have to type the value in manually. If your value is outside of the range the input field gets a red X and there is text information telling the user that the value is not in the accepted range.

Filter Error

 

Setting up the Python Script Tool Metadata

The next step is to enter the metadata for your tool. The metadata allows you to write a description about the tool and detailed information about each of the input values. To edit the metadata right click on the script tool in the Project pane and select View Metadata.

View Script Meta Data

The ArcGIS Pro context sensitive menus will change now that you are viewing metadata. To change the metadata press the Edit button under the Home menu.

Edit Script Meta Data

The first area that appears for editing is the Item Description, Tags and Summary. You can change the item description if it has changed from what you entered when you created the script tool. ArcGIS loves tags so you have to add a tag once you enter this dialogue. The Summary allows you to put a detailed description of what your tool will do.

Add Tags and a Description

Keep scrolling down to find the Dialog Explanation for each of the inputs in your script tool. Here you can write a bit more details about what you want the user to input and what the input will be used for. These pop up if the user hovers on the info button when running the tool.

Add a description to each input

Now our shiny ArcGIS Pro python script tool is all set to go! Lets have a look at it.

Summarize Within Hexbin Grid Script Tool

The tool is all filled out with some of my sample data. When you hover the mouse over one of the input fields a little information i appears. Hovering the mouse over the i gives more details about that script tool from the metadata we filled in. Now the user has more information on how to use the tool to make life easier.

Creating a Python Script for the ArcGIS Pro Script Tool

The next step in this process is to actually write a python script to automate the task we are doing. As a recap we want to generate a hexbin grid that covers our input data set. Summarize the points within the hexbin grid and then produce a new output feature class that has the count of the number of points within each polygon. Load up the python script that your script tool references into your editor of choice.

Importing Arcpy

The first step in a script tool is to import the libraries that you will be using. For this project we need to import arcpy, which is the python library that lets us talk to ArcGIS objects. I also set the environment variable overwriteOutput to true so that existing files will be replaced by our tool. Otherwise the tool could fail if the output already exists.

# Summarize within hex bin script tool
# Author: Ryan Ruthart
# June 1, 2016


import arcpy as arcpy
arcpy.env.overwriteOutput = True

Next we are going to read all of the input values from the tool. This is identical to the way it was set up in ArcGIS Desktop.

#################
# Inputs

featureInput = arcpy.GetParameterAsText(0)
featureOutput = arcpy.GetParameterAsText(1)
featureField = arcpy.GetParameterAsText(2)
hexbinSize = arcpy.GetParameterAsText(3)

arcpy.AddMessage("****************************************")
arcpy.AddMessage("Printing Input Variables")
arcpy.AddMessage("featureInput: {0}".format(featureInput))
arcpy.AddMessage("featureOutput: {0}".format(featureOutput))
arcpy.AddMessage("hexbinSize: {0}".format(hexbinSize))

For each of the fields you will use the arcpy.GetParameterAsText method. This method gets the text value from each of the input fields. The fields are referenced using an index starting at 0. The index increases by 1 for each parameter. We get the input parameters as they were setup in our script tool: Feature Input Class, Feature Output Class, Feature Input Field, Hex Bin Size. To write a message to the console we cannot use the python print method because it does not know how to write to the ArcGIS geoprocessing output console. Instead we use the arcpy.AddMessage() method. This will write the text to our output console.

Here I just write back each of the input variables so that we can have a look at them and see how the string is formatted. You will notice that I am using the string format method to prepare my text. I really like this method as it keeps it clean and simple to format text. Within the string you put curly braces {} and an index number starting at 0. At the end of the string add the .format() method and each value you want to be concatenated into the string. These values will be put into the string starting at index 0.

Here is the output that the start of the script produces:

>****************************************
>Printing Input Variables
>featureInput: C:\Users\ryan\Documents\Ryan\GIS\CalculateChangeTool\Calculate Change Tool\Calculate Change Tool.gdb\CACensus2000
>featureField: Value01
>featureOutput: C:\Users\ryan\Documents\Ryan\GIS\CalculateChangeTool\Calculate Change Tool\Calculate Change Tool.gdb\CACensus2000_SummarizeWithin2
>hexbinSize: 10

So we can see the featureInput has a path and the input file name. The featureOutput also has a path and the output file name. The featureField is printed as a string (if we had picked multiple values they would be separated by a semicolon (;). The hexbinSize is the same number that was input into the tool.

The Arcpy Describe Method

The arcpy.Describe method is a neat feature that can give you more information about a feature class. Go here for detailed information about it. I am going to create a describe object for our input feature class and learn more about this object. I use the describe method to check the dataType (FeatureClass), the shapeType (Point), the path and spatial reference.

featureDesc = arcpy.Describe(featureInput)
arcpy.AddMessage("dataType: {0}".format(featureDesc.dataType)) # Type of feature
arcpy.AddMessage("shapeType: {0}".format(featureDesc.shapeType)) # dataElementType
arcpy.AddMessage("basename: {0}".format(featureDesc.baseName)) ## Base name
arcpy.AddMessage("path: {0}".format(featureDesc.path)) ## GDB Name
arcpy.AddMessage("spatialReference name: {0}".format(featureDesc.spatialReference.name)) # spatialReference

>dataType: FeatureClass
>shapeType: Point
>basename: CACensus2000
>path: C:\Users\ryan\Documents\Ryan\GIS\CalculateChangeTool\Calculate Change Tool\Calculate Change Tool.gdb
>spatialReference name: GCS_North_American_1983

ArcGIS Pro Syntax Errors

This seems like a little bug I noticed. When there is a syntax error in your python script ArcGIS Pro is not currently reporting the line that caused it. Other types of errors are showing the line number, its just not for syntax errors. 

Syntax Error - No Line Number

This can quickly drive you crazy trying to find the line that is broken in  your script. I found a little work around by loading the Python Window under the View menu.

View Python

Then copy and paste your entire code into the Python window.

Python Window Syntax Line Number

When you press enter it will tell you the line that has the invalid syntax. Phew! I expect this will be fixed in the future and was just a slight oversight in ArcGIS Pro. In ArcGIS Desktop syntax errors report the line number.

Running a Geoprocessing Tool to Generate Python Code

One of the easiest ways to figure out how to use an arcpy method is to have ArcGIS produce the code for us. This is another major difference between ArcGIS Desktop and ArcGIS Pro. In ArcGIS Desktop you could create a model and then save it as a python script. After doing a little digging I found this geonet post saying that this feature was removed in ArcGIS Pro 1.1. Luckily there is another way to play around with these tools.

In order to figure out the inputs for my script tool I am going to first run the methods in ArcGIS Pro. First we will look at the ‘Generate Tessellation’ tool which will create the hexbin grid for us. Navigate to the Tool box, search for tessellation and load the tool.

Analysis Tools

I set up the tool to run with my testing dataset and pressed run.

Generate Tesselation

Now here is the little trick to get a arcpy command to run a ArcGIS Pro tool. Under the Project Pane go to ‘Geoprocessing History’. Find the Generate Tessellation command that we just ran and right click>Copy Python command.

Copy Python Command

Lets also load up the documentation page for the Generate Tessellation tool. From the tool documentation we can see that the input is:

GenerateTessellation_management (Output_Feature_Class, Extent, {Shape_Type}, {Size}, {Spatial_Reference})

From copying the Python command for using the tool we see the following:

# GENERATE HEXBIN GRID

arcpy.management.GenerateTessellation(r"C:\Users\ryan\Documents\Ryan\GIS\CalculateChangeTool\Calculate Change Tool\Calculate Change Tool.gdb\GenerateTessellation",
 "-124.127033177439 38.7588919424093 -122.733596327309 40.0017670016459", "HEXAGON", "10 SquareKilometers",
  "GEOGCS['GCS_North_American_1983',DATUM['D_North_American_1983',SPHEROID['GRS_1980',6378137.0,298.257222101]], PRIMEM['Greenwich',0.0],UNIT['Degree',0.0174532925199433]];-400 -400 1111948722.22222;-100000 10000;-100000 10000;8.98315284119521E-

The tool input options include an output feature class, an extent, a shape type, a size and a spatial reference. So which of these do we have and what is missing? This is not our final output so we will generate a temporary feature class which contains the hexbins. The shape type is the shape to use in the generated tessellation, our tool is going to create a hexbin grid so we have that value. The extent we want to use will cover all of the points in our input file. We should be able to get it, but we don’t have it in the right format quite yet. The size of the hexbin is also from the input file. The spatial reference we would like to use can be gathered from the describe object of the input feature class. So the only piece of data we really need to figure out is the extent of the point feature class. 

Creating a Function to Determine the Input Feature Class Extent

In our python script it is possible to create functions to help us keep code clean and break it down into smaller reusable parts. Looking at the python code we copied from the geoprocessing history we can see that the extent is input as follows:

“-124.127033177439 38.7588919424093 -122.733596327309 40.0017670016459”

So the format is “minX, minY, maxX, maxY”. We need to generate these values for the extent covered by the points in our input file. I am going to create a function that takes an input feature class and determines its extent. The code is below.

def getFeatureClassExtent(fc):
  
  counter = 0
  for row in arcpy.da.SearchCursor(fc, ["SHAPE@"]):
    extent = row[0].extent
    if counter == 0:
      XMin = extent.XMin
      XMax = extent.XMax
      YMin = extent.YMin
      YMax = extent.YMax

    if extent.XMin < XMin:
      XMin = extent.XMin
    if extent.XMax > XMax:
      XMax = extent.XMax
    if extent.YMin < YMin:
      YMin = extent.YMin
    if extent.YMax > YMax:
      YMax = extent.YMax
    counter += 1
  return "{0} {1} {2} {3}".format(XMin, YMin, XMax, YMax)

The getFeatureClassExtent function takes a feature class as its input. I then creates an arcpy SearchCursor to search through every row in the input feature class. I am using two inputs for the SearchCursor. The first is the feature class, and the second is a list containing the fields I want to pull from the table. I am returning the “SHAPE@” field which returns a geometry object for the row. The geometry object has a method called extent which can return the XMin, XMax, YMin and YMax for that geometry. On the first pass through the loop I set the min and max values to the current row value. Then I continue to loop through all of the rows in the input feature class and compare the new values to the current values. If the min is lower or the max is higher I replace the values. When the entire feature class has been looped through we know the extent for the feature class. I then use string format method to format the results the way that the GenerateTessellation tool requires.

We now call the getFeatureClassExtent tool to get the inputExtent. I also mentioned that we know the spatial reference for the input polygon, but how did I get it? It was contained in the describe object we created earlier — I told you that was a handy tool didn’t I? When I call the function I use the .spatialReference method to return the spatial reference of the input feature class. I also created a temporary feature class (tempGrid) to generate the empty hexbin grid into (again I used the describe object and string formatting to format it). Now we can generate our tessellation! 

inputExtent = getFeatureClassExtent(featureInput)
arcpy.AddMessage("Extent: {0}".format(inputExtent))
tempGrid = "{0}//{1}_TempHex".format(featureDesc.path, featureDesc.baseName)

# GENERATE HEXBIN GRID

arcpy.management.GenerateTessellation(tempGrid, inputExtent, "HEXAGON", "{0} SquareKilometers".format(hexbinSize), 
  featureDesc.spatialReference)

Generating the Python Code to Run ‘Summarize Within’

I use a very similar procedure as above to figure out the python code to run the Summarize Within Tool. First I went to the Toolbox, searched for Summarize Within and loaded the tool. I filled the tool in with my test input files and ran the tool. I didn’t chose to summarize a field, and picked ‘Add shape summary attributes’. With this option selected it will count the number of points in each hex polygon.

Summarize Within Tool

Then back to the Project Pane>Geoprocessing History>Right Click>Copy Python Command. I also went to the Summarize Within help page and got the syntax for this function. The syntax from the help page is:

SummarizeWithin_analysis (in_polygons, in_sum_features, out_feature_class, {keep_all_polygons}, {sum_fields}, {sum_shape}, {shape_unit}, {group_field}, {add_min_maj}, {add_group_percent}, {out_group_table})

The script from the Python Command was:

# arcpy.analysis.SummarizeWithin("CACensus2000_TempHex", "CACensusPoints_Hotspot_2010", 
# 	r"C:\Users\ryan\Documents\Ryan\GIS\CalculateChangeTool\Calculate Change Tool\Calculate Change Tool.gdb\CACensus2000_TempHex_Summari", 
# 	"ONLY_INTERSECTING", None, "ADD_SHAPE_SUM", None, None, "NO_MIN_MAJ", "NO_PERCENT", None)

The only values I am going to change are in_polygons, in_sum_features, out_feature_class. For in_polygons I am using the hexbin grid generated in the previous step. The in_sum_features is the point feature class from our tool and the out_feature_class is the output from our tool. The rest of the values I kept the same as was copied from the python tool in order to get the same results. The command is now ready for our script tool.

arcpy.analysis.SummarizeWithin(tempGrid, featureInput, featureOutput,
  "ONLY_INTERSECTING", None, "ADD_SHAPE_SUM", None, None, "NO_MIN_MAJ", "NO_PERCENT", None)

So that is the majority of the code we need to automate the workflow I set out. There are a few final steps I am going to take to wrap things up.

Clean up the Temporary File with Error Checking

We generated a temporary table which had the empty hexbin grid during the process. I would like to delete this table if possible. I also want to use error checking, i.e., a try except block so that if deleting fails the script tool will still run successfully. The code is as follows:

try:
  arcpy.Delete_management(tempGrid)
except Exception:
  e = sys.exc_info()[1]
  arcpy.AddMessage(e.args[0])

 Add a Check the Input Feature Class is a Points File

The last thing I want to check for is that the input feature class contains points. Since this tool is designed to count the number of points within a hexbin grid we should definitely make sure we have the right input. This way the tool wont fail if the user puts lines or polygons as the input feature class. This will use the describe method and the shapeType property.

if featureDesc.shapeType == "Point":

 Perfect! Now the python code for our script tool is complete.

Finalized Python Arcpy Code for the Script Tool

All right! That was pretty easy wasn’t it. Here is the complete python code:

# Summarize within hex bin script tool
# Author: Ryan Ruthart
# June 1, 2016


import arcpy as arcpy
arcpy.env.overwriteOutput = True
#################
# Functions
def getFeatureClassExtent(fc):
  
  counter = 0
  for row in arcpy.da.SearchCursor(fc, ["SHAPE@"]):
    extent = row[0].extent
    if counter == 0:
      XMin = extent.XMin
      XMax = extent.XMax
      YMin = extent.YMin
      YMax = extent.YMax

    if extent.XMin < XMin:
      XMin = extent.XMin
    if extent.XMax > XMax:
      XMax = extent.XMax
    if extent.YMin < YMin:
      YMin = extent.YMin
    if extent.YMax > YMax:
      YMax = extent.YMax
    counter += 1
  return "{0} {1} {2} {3}".format(XMin, YMin, XMax, YMax)

#################
# Inputs

featureInput = arcpy.GetParameterAsText(0)
featureOutput = arcpy.GetParameterAsText(1)
featureField = arcpy.GetParameterAsText(2)
hexbinSize = arcpy.GetParameterAsText(3)

arcpy.AddMessage("****************************************")
arcpy.AddMessage("Printing Input Variables")
arcpy.AddMessage("featureInput: {0}".format(featureInput))
arcpy.AddMessage("featureField: {0}".format(featureField))
arcpy.AddMessage("featureOutput: {0}".format(featureOutput))
arcpy.AddMessage("hexbinSize: {0}".format(hexbinSize))

# Create a description of the input file
# This is an easy way to get useful information about a feature class that can be used
# to make decisions in your code
featureDesc = arcpy.Describe(featureInput)
arcpy.AddMessage("dataType: {0}".format(featureDesc.dataType)) # Type of feature
arcpy.AddMessage("shapeType: {0}".format(featureDesc.shapeType)) # dataElementType
arcpy.AddMessage("basename: {0}".format(featureDesc.baseName)) ## Base name
arcpy.AddMessage("path: {0}".format(featureDesc.path)) ## GDB Name
arcpy.AddMessage("spatialReference name: {0}".format(featureDesc.spatialReference.name)) # spatialReference


################
# Actual Methods

if featureDesc.shapeType == "Point": # Check the input feature class contains points

  inputExtent = getFeatureClassExtent(featureInput)
  arcpy.AddMessage("Extent: {0}".format(inputExtent))
  tempGrid = "{0}//{1}_TempHex".format(featureDesc.path, featureDesc.baseName)

  # GENERATE HEXBIN GRID
  arcpy.management.GenerateTessellation(tempGrid, inputExtent, "HEXAGON", "{0} SquareKilometers".format(hexbinSize), 
    featureDesc.spatialReference)

  # SUMMARIZE WITHIN HEXBIN GRID
  arcpy.analysis.SummarizeWithin(tempGrid, featureInput, featureOutput,
    "ONLY_INTERSECTING", None, "ADD_SHAPE_SUM", None, None, "NO_MIN_MAJ", "NO_PERCENT", None)

  # Remove temporary data using a try except
  # This way if the data fails to delete it will not break the tool
  try:
    arcpy.Delete_management(tempGrid)
  except Exception:
    e = sys.exc_info()[1]
    arcpy.AddMessage(e.args[0])
else:
  arcpy.AddMessage("This tool is only designed to work with a point input file")

Notice that on line 61 I added the if statement to check that the input feature class contains points. If this is not the case a message is returned to the user. Now we can run the code and produce a hexbin point count for any shapefile. Instead of having to run all of those steps separately they are ready to go in one automated tool.

Here is a zip file with the python script and the summarize change tool. You can use this to try the tool with your own data.

I hope this guide was useful introducing you to creating an ArcGIS Pro python script tool to automate a process. If you have any questions or comments (or just love hexbin grids and want to shout out to the world) let me know.