Data access using cursors

A cursor is a data access object that can be used to either iterate over the set of rows in a table or insert new rows into a table. Cursors have three forms: search, insert, and update. Cursors are commonly used to read existing geometries and write new geometries.

Each type of cursor is created by a corresponding ArcPy function (SearchCursor, InsertCursor, or UpdateCursor) on a table, table view, feature class, or feature layer. A search cursor can be used to retrieve rows. An update cursor can be used to update and delete rows, while an insert cursor is used to insert rows into a table or feature class.

CursorExplanation

arcpy.da.InsertCursor(in_table, field_names)

Inserts rows

arcpy.da.SearchCursor(in_table, field_names, {where_clause}, {spatial_reference}, {explode_to_points}, {sql_clause})

Read-only access

arcpy.da.UpdateCursor(in_table, field_names, {where_clause}, {spatial_reference}, {explode_to_points}, {sql_clause})

Updates or deletes rows

Data access cursor functions (arcpy.da)
Legacy:

The data access module (arcpy.da) was added in ArcGIS 10.1. The original cursors are still supported; however, the new arcpy.da cursors include significantly faster performance. In most cases, the help documentation describes the use of the arcpy.da cursors. For more information about the classic cursor model, see the InsertCursor, SearchCursor, and UpdateCursor topics.

Note:

Cursors honor layer and table view definition queries and selections. The cursor object only contains the rows that would be used by any geoprocessing tool during an operation.

Cursors can only be navigated in a forward direction; they do not support backing up and retrieving rows that have already been retrieved. If a script needs to make multiple passes over the data, the cursor's reset method may be called.

Search or update cursors can be iterated with a for loop. The next row can also be accessed by explicitly using the next method to return the next row. When using the next method on a cursor to retrieve all rows in a table containing N rows, the script must make N calls to next. A call to next after the last row in the result set has been retrieved returns a StopIteration exception.

import arcpy

cursor = arcpy.da.SearchCursor(fc, ['fieldA', 'fieldB'])
for row in cursor:
    print(row)

Search and update cursors also support with statements.

import arcpy

with arcpy.da.SearchCursor(fc, ['fieldA', 'fieldB']) as cursor:
    for row in cursor:
        print(row)

Each row retrieved from a table is returned as a list of field values. The values will be returned in the same order as provided to the cursor's field_names argument. A cursor's fields property can also be used to confirm the order of field values.

Cursor object

SearchCursor, UpdateCursor, and InsertCursor create a cursor object that can be used to iterate through the records. The methods of the cursor object created by the various cursor functions vary depending on the type of cursor created.

The following chart shows the methods supported by each cursor type:

Cursor typeMethod

arcpy.da.SearchCursor

reset—Resets the cursor to its starting position.

arcpy.da.InsertCursor

insertRow—Inserts a row into the table.

arcpy.da.UpdateCursor

updateRow—Updates the current row.

deleteRow—Removes the row from the table.

reset—Resets the cursor to its starting position.

insertRow

An insert cursor is used to create rows and insert them. Once the cursor has been created, the insertRow method is used to insert a list (or tuple) of values that will make up the new row. Any field in the table that is not included in the cursor will be assigned the field's default value.

import arcpy

# Create insert cursor for table
cursor = arcpy.da.InsertCursor("c:/base/data.gdb/roads_lut", 
                               ["roadID", "distance"])

# Create 25 new rows. Set the initial row ID and distance values
for i in range(0,25):
    cursor.insertRow([i, 100])

updateRow

The updateRow method is used to update the row at the current position of an update cursor. After returning a row from the cursor object, you can modify the row as needed and call updateRow, passing in the modified row.

import arcpy

# Create update cursor for feature class
with arcpy.da.UpdateCursor("c:/base/data.gdb/roads",
                           ["roadtype", "distance"]) as cursor:
    for row in cursor:
        # Update the values in the distance field by multiplying 
        #   the roadtype by 100. Road type is either 1, 2, 3 or 4.
        row[1] = row[0] * 100
        cursor.updateRow(row)

deleteRow

The deleteRow method is used to delete the row at the current position of an update cursor. After fetching the row, call deleteRow on the cursor to delete the row.

import arcpy

# Create update cursor for feature class
with arcpy.da.UpdateCursor("c:/base/data.gdb/roads", 
                          ["roadtype"]) as cursor:
    # Delete all rows that have a roads type of 4
    for row in cursor:
        if row[0] == 4:
            cursor.deleteRow()

Accessing and setting field values

For each cursor, the fields used are supplied by a list (or tuple) of field names. When a row is returned from the cursor, it is returned as a list of field values that correspond by index position.

In the sample below, state name and population count are accessed by position.

import arcpy

fc = "c:/base/data.gdb/USA/States"

# Use SearchCursor to access state name and the population count
with arcpy.da.SearchCursor(fc, ['STATE_NAME', 'POP2000']) as cursor:
    for row in cursor:
        # Access and print the row values by index position.
        #   state name: row[0]
        #   population: row[1]
        print('{} has a population of {}'.format(row[0], row[1]))
Tip:

While all fields can be accessed using an asterisk (*), it is not generally recommended. The more fields specified, the slower the cursor will perform. Listing only the fields you are expecting to use will improve the overall efficiency of the cursor.

Short and long field values are returned as integers. Float and double field values are returned as floats. Text field types are returned as strings. More information on additional field types is described below.

Object identifier fields

When you create a table in ArcGIS, a unique, nonnullable integer field is added to the table to act as an object identifier (object ID). The object ID field is maintained by ArcGIS and guarantees a unique ID for each row in the table.

While all tables include an object ID field, the name of the field depends on the data type. The OBJECTID@ token can also be used as a shortcut in place of a table's object ID field name.

Geometry fields

In ArcGIS, the geometry data type indicates the type of geometry—point, line, polygon, multipoint, or multipatch—that the table stores. The geometry field stored is typically, but not always, named Shape.

Tokens can also be used as shortcuts in place of the geometry field name. The SHAPE@ token, which returns a geometry object, can be used to access the geometry field of a feature class without having prior knowledge of the field name.

Print the x,y coordinates of a point feature class using a search cursor.

import arcpy

infc = "c:/data/fgdb.gdb/fc"

# Enter a for loop for each feature
with arcpy.da.SearchCursor(infc, ['OID@', 'SHAPE@']) as cursor:
    for row in cursor:
        # Print the current point's object ID and x,y coordinates
        print(f'Feature {row[0]}: {row[1][0].X}, {row[1][0].Y}')

Additional geometry tokens can be used to access specific geometry information. Accessing the full geometry is more time-consuming. If you only need specific properties of the geometry, use tokens to provide shortcuts to access geometry properties. For instance, SHAPE@XY will return a tuple of x,y coordinates that represent the feature's centroid.

To learn more about accessing geometry in cursors, see Read geometries and Write geometries.

Global identifier fields

Global ID and GUID field types store registry-style strings consisting of 36 characters enclosed in curly brackets. These strings uniquely identify a feature or table row in a geodatabase and across geodatabases.

In a cursor, GUID fields can accept string and universally unique identifier (UUID) values. When reading Global ID and GUID fields, a string is returned.

Add a record with a GUID field using a UUID value.

import arcpy
import uuid

ic = arcpy.da.InsertCursor("c:/data/fgdb.gdb/fc", ['guid_field'])
id = uuid.uuid1()
ic.insertRow([id])

Add a record with a GUID field using a string.

import arcpy

ic = arcpy.da.InsertCursor("c:/data/fgdb.gdb/fc", ['date_field'])
id = '0345e170-2614-11eb-812d-f8b156b0d357'
ic.insertRow([id])

Date fields

Date fields can store dates, times, or dates and times.

In a cursor, a date field can accept a datetime object or a string value. When reading date fields, a datetime object is returned.

Add a record with a date field using a datetime object.

import arcpy
from datetime import datetime

ic = arcpy.da.InsertCursor("c:/data/fgdb.gdb/fc", ['date_field'])
date_value = datetime(2020, 7, 1, 12, 30)
ic.insertRow([date_value])

Add a record with a date field using a string value.

import arcpy

ic = arcpy.da.InsertCursor("c:/data/fgdb.gdb/fc", ['date_field'])
date_value = "July 1 2020 12:30)
ic.insertRow([date_value])

BLOB fields

A binary large object (BLOB) is data stored as a long sequence of binary numbers. ArcGIS stores annotation and dimensions as BLOBs, and items such as images, multimedia, or bits of code can be stored in this type of field. You can use a cursor to load or view the contents of a BLOB field.

In a cursor, BLOB fields can accept strings, bytes objects, and memoryview objects. When reading BLOB fields, a memoryview object is returned.

Add a record with a BLOB field using a .png file.

import arcpy

data = open("c:/images/image1.png", "rb").read()
ic = arcpy.da.InsertCursor("c:/data/fgdb.gdb/fc", ['imageblob'])
ic.insertRow([data])

Read a BLOB field as a memoryview object and write to a .png file.

import arcpy

sc = arcpy.da.SearchCursor("c:/data/fgdb.gdb/fc", ["imageblob"])
memview = sc.next()[0]
open("c:/images/image1_copy.png", "wb").write(memview.tobytes())

Raster fields

A raster field can store raster data in or alongside the geodatabase.

In a search cursor, values from a raster field are read as a Raster object. Update and insert cursor do not support raster fields.

Read a raster field and write to a .tif file.

import arcpy

sc = arcpy.da.SearchCursor("c:/data/fgdb.gdb/fc", ['imag'])
raster = sc.next()[0]
raster.save("c:/images/image.tiff")

Cursors and locking

Insert and update cursors honor table locks set by ArcGIS applications. Locks prevent multiple processes from changing the same table at the same time. There are two types of locks, shared and exclusive, described as follows:

  • A shared lock is applied anytime a table or dataset is accessed. Multiple shared locks can exist for a table, but no exclusive locks are permitted if a shared lock exists. Displaying a feature class and previewing a table are examples of when a shared lock would be applied.
  • Exclusive locks are applied when changes are made to a table or feature class. Editing and saving a feature class in a map, changing a table's schema, or using an insert cursor on a feature class in a Python IDE are examples of when an exclusive lock is applied by ArcGIS.

Update and insert cursors cannot be created for a table or feature class if an exclusive lock exists for that dataset. The UpdateCursor or InsertCursor function fails because of an exclusive lock on the dataset. If these functions successfully create a cursor, they apply an exclusive lock on the dataset so that two scripts cannot create an update or insert cursor on the same dataset.

Cursors support with statements to reset iteration and aid in removal of locks. However, using a del statement to delete the object or wrapping the cursor in a function to have the cursor object go out of scope should be considered to guard against all locking cases.

An edit session applies a shared lock to data during the edit session. An exclusive lock is applied when edits are saved. A dataset is not editable if an exclusive lock already exists.