ArcGIS Pro Sequentially Increment Numbered Values with ArcPy

Many years ago I came across the How to Create Sequential Numbers in a Field in ArcMap using Python in the Field Calculator from Esri Technical Support and I have used this as the basis for more complex numbering throughout the years for various different projects. Our workflow for this blog post will use a tailored code block based on the code block from original article, and add some extra functionality so we can see how ArcPy can be employed to rapidly assign a custom incremented ID attribute to a feature class or table. We will offer the user a choice to simply add incremented digits, pad the output with zeros, and the slightly more complex; to assign a prefix based on text input or from values in a field. We will also give the user the choice to group features and to start at 1 for each group and increment.

If you are interested in learning ArcPy, check out this course!

We start, as always, by importing the ArcPy module.

import arcpy

We will need two required user input parameters; in_table as a Feature Layer or Table, and fld_name, which can be an existing text field or a new field to create. We have some optional parameters to cater for; group_fld is the field that contains the values to base groups from, prefix can be a field and we will use the field value as a prefix or the user can add text for an all encompassing prefix value, padding allows the user to add padded zeros so 123 with a padding of 5 will become 00123, and the fld_length paramter will be hidden for now and given a default of 100. In a future release we will utilise this parameter for more vigorous validation, but for now, the maximum length for a new text field from the fld_name parameter will be 100. When using the prefix parameter a hyphen (-) is automatically added to separate the prefix and the number components (eg PREFIX-00123)

## input feature layer or table from the APRX
in_table = arcpy.GetParameter(0)

## text field to update. If the field doesnt exist, create it
fld_name = arcpy.GetParameterAsText(1)

## start the count at 1 for each group identifues
group_fld = arcpy.GetParameterAsText(2)

## enter a prefix or use a field attribute, a - (hyphen) will be added to
## separate the prefix and the numbers
prefix = arcpy.GetParameterAsText(3)

## padd out the number part with zeros eg. 5 00001, 01234
## default 0
padding = arcpy.GetParameterAsText(4)

## a hidden parameter to help decide on the minimum length of the text field
## based on the number of features and numbers required
fld_length = arcpy.GetParameterAsText(5)

We have the need for two functions. The first returns a list of unique values for a supplied field.

def getUniqueValues(table, field):
    """
    Get a list of unique values from a single field in a table

    Args:
        table (str):  table or feature class in a geodatabase
        field (str):  field to get unique values from

    Returns:
        a sorted set of unique entries
    """
    with arcpy.da.SearchCursor(table, [field]) as cursor:
        return sorted({row[0] for row in cursor if row[0] != None})

The second, performs the calculations to add the sequentially incremented values. We have set the starting number to 1 and also to increment each value by 1. You can alter this as required or even add them as a parameter for users to set themselves.

def incrementValues(features):
    """
    Increment the

    Args:
        features:  features to use for adding incremented value to

    Returns:
       None
    """

    arcpy.management.CalculateField(
        in_table = features,
        field = fld_name,
        expression = "sequentialIncremental(1,1,{0},{1})".format(prefix, padding),
        expression_type = "PYTHON3",
        code_block = code_block
    )

We only have a couple required objects to help with the workflow. We create an ArcGISProject object for the open APRX. We then check whether the input from our first parameter (in_table) is a Layer or a Table and use that ArcGISProject object to access the layer or table object via the active map.

## access the APRX
aprx = arcpy.mp.ArcGISProject("CURRENT")

## figure out if input is a layer or table and access the layer or table
## in the active map
if isinstance(in_table, arcpy._mp.Layer):
    in_table = aprx.activeMap.listLayers(in_table.longName)[0]

elif isinstance(in_table, arcpy._mp.Table):
    in_table = aprx.activeMap.listTables(in_table.longName)[0]

Next up is the code block we will use to calculate the sequentially incremented values. You can check out the original code from the link in the opening sentence of this blog post. The code block below has built upon it to factor in the prefix and the padding values.

code_block="""
num = 0
def sequentialIncremental(start_num, increment_by, prefix, padding):
    global num
    if num == 0:
        num = start_num
    else:
        num += increment_by

    if prefix:
        return "{0}-{1}".format(prefix, str(num).zfill(padding))
    else:
        return "{0}".format(str(num).zfill(padding))
"""

We need to perform a couple of field checks from our inputs. If the fld_name supplied does not already exist as a field, then add the field to the layer/table. We also need to check whether we are using a supplied static text prefix or if we are using the value from a defined field.

## check if the fld_name exists
fld_list = [fld.name for fld in arcpy.ListFields(in_table) if fld.name == fld_name]

## if the fld name does not exist create a field to store the incrementals
if not fld_list:
    arcpy.AddMessage("Adding Field")

    arcpy.management.AddField(
        in_table = in_table,
        field_name = fld_name,
        field_type = "TEXT",
        field_length = fld_length
    )

## sort out the prefix, whether it is static for all features or if we use
## an attribute as the prefix
fld_list = [fld.name for fld in arcpy.ListFields(in_table) if fld.name == prefix]

if fld_list:
    prefix = "!{0}!".format(fld_list[0])
else:
    prefix = "'{0}'".format(prefix)

We’re now ready for the big leagues. It’s time to increment! If a Group-by Field has been set we get a subset of features that matches each unique value from the supplied group_fld parameter and for each group the starting number is 1 and using any user defined inputs such as prefix and/or padding. If no Group-by Field is set then we consider the entire set of features and increment based on the user inputs.

arcpy.AddMessage("Calculating incremented values")

## if a Group-by Field has been set
if group_fld:
    ## get all unique values for the Group-by field
    values = getUniqueValues(in_table, group_fld)

    ## for each unique value found (does not account for nulls)
    for value in values:

        ## get a selection of records that are in the group
        selection = arcpy.management.SelectLayerByAttribute(
            in_layer_or_view = in_table,
            selection_type = "NEW_SELECTION",
            where_clause = "{0} = '{1}'".format(group_fld, value)
        )

        ## add the incremented values for the selection
        incrementValues(selection)

        ## clear the selection
        arcpy.management.SelectLayerByAttribute(
                in_layer_or_view = in_table,
                selection_type = "CLEAR_SELECTION"
        )

## if no group set, consider all features for the same treatment
else:
    incrementValues(in_table)

This is a nice tool and has the potential for more functionality and added validations and we will look at adding these at a future date, you might even add some yourself and let us know. We like the idea of being able to chose a field or fields to sort by before performing the calculations of the incremented values, at the moment they are based on sorted OIDs (even though we did not explicitly tell it to do this, it is how the Calculate Field tool operates).

Save your script and open up ArcGIS Pro. Right-click on your toolbox/toolset of choice and select New > Script. The New Script window will appear. In the General tab set Name to sequentialInrementalLabel to Sequentially Increment Attributes, and the Description as per below or any description you feel is apt.

In the Parameters tab set as below. Set the Data Type for the Input Layer or Table to Feature Layer, Table View. For the Increment Field, set the Filter to Text field only, and the Dependency to in_table. For the Group By Field set the Type to Optional, the Filter to Short, Long, Text fields, and the Dependency to in_table. For the Prefix, set the Type to Optional, the Filter to Text fields only and the Dependency to in_table. For Padding, set the Type to Optional and the Default to 0 (Zero), and for the New Field Length, set the Default to 100.

In the Execution tab, click the folder icon in the top-right corner and add your saved Python script.

In the Validation tab, set as per below. When the tool is opened we want to hide the New Field Length parameter. We also want users to be able to add a new field name for the incremented values and for a prefix so we remove any error message caused by not selecting a current field name.

def initializeParameters(self):
    # Customize parameter properties. This method gets called when the
    # tool is opened.
    self.params[5].enabled = False
    return

    def updateMessages(self):
        # Modify the messages created by internal validation for each tool
        # parameter. This method is called after internal validation.
        if self.params[1].value:
            self.params[1].clearMessage()
        if self.params[3].value:
            self.params[3].clearMessage()
        return

You can download the tool and other custom tools over on this page. This tool is in the Custom Tools on a Basic License with ArcPy section.

Here’s the script above in its entirety.

import arcpy

################################################################################
## Documentation Links:
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/functions/getparameter.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/functions/getparameterastext.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/data-access/searchcursor-class.htm
##  https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/calculate-field.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/mapping/arcgisproject-class.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/mapping/map-class.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/functions/listfields.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/classes/field.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/mapping/layer-class.htm
##  https://pro.arcgis.com/en/pro-app/latest/tool-reference/data-management/select-layer-by-attribute.htm
##  https://pro.arcgis.com/en/pro-app/3.2/arcpy/functions/addmessage.htm
##  https://support.esri.com/en/technical-article/000011137
##
## ArcGIS Pro 3.2.0
##
################################################################################

################################################################################
## USER INPUTS #################################################################

## input feature layer or table from the APRX
in_table = arcpy.GetParameter(0)

## text field to update. If the field doesnt exist, create it
fld_name = arcpy.GetParameterAsText(1)

## start the count at 1 for each group identifues
group_fld = arcpy.GetParameterAsText(2)

## enter a prefix or use a field attribute, a - (hyphen) will be added to
## separate the prefix and the numbers
prefix = arcpy.GetParameterAsText(3)

## padd out the number part with zeros eg. 5 00001, 01234
## default 0
padding = arcpy.GetParameterAsText(4)

## a hidden parameter to help decide on the minimum length of the text field
## based on the number of features and numbers required
fld_length = arcpy.GetParameterAsText(5)

################################################################################
## FUNCTIONS ###################################################################

def getUniqueValues(table, field):
    """
    Get a list of unique values from a single field in a table

    Args:
        table (str):  table or feature class in a geodatabase
        field (str):  field to get unique values from

    Returns:
        a sorted set of unique entries
    """
    with arcpy.da.SearchCursor(table, [field]) as cursor:
        return sorted({row[0] for row in cursor if row[0] != None})

def incrementValues(features):
    """
    Increment the

    Args:
        features:  features to use for adding incremented value to

    Returns:
       None
    """

    arcpy.management.CalculateField(
        in_table = features,
        field = fld_name,
        expression = "sequentialIncremental(1,1,{0},{1})".format(prefix, padding),
        expression_type = "PYTHON3",
        code_block = code_block
    )

################################################################################
## REQUIRED OBJECTS ############################################################

## access the APRX
aprx = arcpy.mp.ArcGISProject("CURRENT")

## figure out if input is a layer or table and access the layer or table
## in the active map
if isinstance(in_table, arcpy._mp.Layer):
    in_table = aprx.activeMap.listLayers(in_table.longName)[0]

elif isinstance(in_table, arcpy._mp.Table):
    in_table = aprx.activeMap.listTables(in_table.longName)[0]

################################################################################
## CODE BLOCK ##################################################################

code_block="""
num = 0
def sequentialIncremental(start_num, increment_by, prefix, padding):
    global num
    if num == 0:
        num = start_num
    else:
        num += increment_by

    if prefix:
        return "{0}-{1}".format(prefix, str(num).zfill(padding))
    else:
        return "{0}".format(str(num).zfill(padding))
"""

################################################################################
## FIELD CHECKS ################################################################

## check if the fld_name exists
fld_list = [fld.name for fld in arcpy.ListFields(in_table) if fld.name == fld_name]

## if the fld name does not exist create a field to store the incrementals
if not fld_list:
    arcpy.AddMessage("Adding Field")

    arcpy.management.AddField(
        in_table = in_table,
        field_name = fld_name,
        field_type = "TEXT",
        field_length = fld_length
    )

## sort out the prefix, whether it is static for all features or if we use
## an attribute as the prefix
fld_list = [fld.name for fld in arcpy.ListFields(in_table) if fld.name == prefix]

if fld_list:
    prefix = "!{0}!".format(fld_list[0])
else:
    prefix = "'{0}'".format(prefix)

################################################################################
## SEQUENTIAL INCREMENTAL ######################################################

arcpy.AddMessage("Calculating incremented values")

## if a Group-by Field has been set
if group_fld:
    ## get all unique values for the Group-by field
    values = getUniqueValues(in_table, group_fld)

    ## for each unique value found (does not account for nulls)
    for value in values:

        ## get a selection of records that are in the group
        selection = arcpy.management.SelectLayerByAttribute(
            in_layer_or_view = in_table,
            selection_type = "NEW_SELECTION",
            where_clause = "{0} = '{1}'".format(group_fld, value)
        )

        ## add the incremented values for the selection
        incrementValues(selection)

        ## clear the selection
        arcpy.management.SelectLayerByAttribute(
                in_layer_or_view = in_table,
                selection_type = "CLEAR_SELECTION"
        )

## if no group set, consider all features for the same treatment
else:
    incrementValues(in_table)

################################################################################

Leave a Comment

Your email address will not be published. Required fields are marked *