Select to view content in your preferred language

Repeating Tuples with Insert/Search Cursor

233
5
Jump to solution
Friday
KevinDickinson1
Emerging Contributor

I am trying to use an InsertCursor/SeachCursor to transfer data from one dataset to another wile incorporating a repeating tuple. The first dataset (in the below code is "Forest_Surface_Fuels") contains all the data and has attribute fields for each record that contains data for 1hr, 10hr, and 100hr fuels. What I am trying to do as I transfer this data to a empty dataset ("1_10_100") is to not only take the values in those fields, but create a master list for all records in a project and in a new field state whether the data is a 1hr, 10hr, or 100hr.

Here is the code that I have:

# Fields that data will be inserted into in the final excel table
fields = ['Plot_', 'SurveyType', 'Transect', 'OneHour', 'TenHr', 'HunHr']

#The data to be inserted (this is the tuple that will be repeated)
fuelsTuple = [
        ('Macroplot', "Pre", "1", 'onehr_pre_1', 'tenhr_pre_1', 'hundhr_pre_1'),
        ('Macroplot', "Pre", "2", 'onehr_pre_2', 'tenhr_pre_2', 'hundhr_pre_2'),
        ('Macroplot', "Post", "1", 'onehr_post_1', 'tenhr_post_1', 'hundhr_post_1'),
        ('Macroplot', "Post", "2", 'onehr_post_2', 'tenhr_post_2', 'hundhr_post_2')
]

#make count of times to repeat the tuple
repeats = arcpy.management.GetCount("Forest_Surface_Fuels")
count = int(repeats[0])

#Insert Cursor to update fields
plotCursor = arcpy.da.InsertCursor("Forest_Surface_Fuels", fuelsTuple)
with arcpy.da.SearchCursor("1_10_100", fields) as cursor:
       for i in range(count):
             for row in cursor:
                   plotCursor.insertRow(row)
del plotCursor
del cursor

When I run this, I get this error:

Screenshot 2025-12-26 155449.png

Thanks!

0 Kudos
2 Solutions

Accepted Solutions
VenkataKondepati
Regular Contributor

You’re very close, but the cursors are flipped and you’re passing the tuple list as “fields” (that’s why it blows up).

  • SearchCursor should read from Forest_Surface_Fuels (source)

  • InsertCursor should write to 1_10_100 (target)

  • InsertCursor("Forest_Surface_Fuels", fuelsTuple) is wrong because the 2nd argument must be a list of field names, not row data.

If your goal is: for each input feature, create 4 rows (Macroplot Pre/Post x Transect 1/2) and pull values from those named fields, do this pattern:

out_fields = ['Plot_', 'SurveyType', 'Transect', 'OneHour', 'TenHr', 'HunHr']

templates = [
    ("Pre","1", "onehr_pre_1","tenhr_pre_1","hundhr_pre_1"),
    ("Pre","2", "onehr_pre_2","tenhr_pre_2","hundhr_pre_2"),
    ("Post","1","onehr_post_1","tenhr_post_1","hundhr_post_1"),
    ("Post","2","onehr_post_2","tenhr_post_2","hundhr_post_2"),
]

# read Plot_ plus all fuel fields you reference
src_fields = ['Plot_'] + sorted({f for t in templates for f in t[2:]})

with arcpy.da.SearchCursor("Forest_Surface_Fuels", src_fields) as sc, \
     arcpy.da.InsertCursor("1_10_100", out_fields) as ic:

    idx = {name:i for i,name in enumerate(src_fields)}

    for r in sc:
        plot_id = r[idx['Plot_']]
        for survey, tran, f1, f10, f100 in templates:
            ic.insertRow([plot_id, survey, tran, r[idx[f1]], r[idx[f10]], r[idx[f100]]])

 

That gives you the “master table” (one row per plot per survey/transect) with 1/10/100 hr values populated correctly.

View solution in original post

HaydenWelch
MVP Regular Contributor

I'd approach this by making the field tuple a bit more complex. It seems that the first records are raw data and the last few are field values. You can just break it into two mappings like this:

# First Dictionary is direct data writes, second is field values to pull from the Origin dataset
conversion_map: list[tuple[dict[str, Any], dict[str, Any]]] = [
    (
        {'Plot_': 'Macroplot', 'SurveyType': 'Pre', 'Transect': '1'}, 
        {'OneHour': 'onehr_pre_1', 'TenHr': 'tenhr_pre_1', 'HunHr': 'hundhr_pre_1'}
    ),
    (
        {'Plot_': 'Macroplot', 'SurveyType': 'Pre', 'Transect': '2'}, 
        {'OneHour': 'onehr_pre_2', 'TenHr': 'tenhr_pre_2', 'HunHr': 'hundhr_pre_2'}
    ),
    (
        {'Plot_': 'Macroplot', 'SurveyType': 'Post', 'Transect':'1'}, 
        {'OneHour': 'onehr_post_1', 'TenHr': 'tenhr_post_1', 'HunHr': 'hundhr_post_1'}
    ),
    (
        {'Plot_': 'Macroplot', 'SurveyType': 'Post', 'Transect':'2'}, 
        {'OneHour': 'onehr_post_2', 'TenHr': 'tenhr_post_2', 'HunHr': 'hundhr_post_2'}
    ),
]

That will make it clearer later what the raw data mapping is and what the extracted field values are.

When dealing with multiple cursors, I tend to like mapping the rows instead of using an index. This can be done with this incredibly simple helper function:

def as_dict(cur: SearchCursor) -> Iterator[dict[str, Any]]:
    for row in cur:
        yield dict(zip(cur.fields, row))

You can wrap a cursor with this before iterating and you'll get back a dictionary that can be updated

 Now we need to implement the mapping:

def convert(origin: str, target: str, fields: list[str], conversion_map: ConversionMap, null_value: Any=None):
    with (
        SearchCursor(origin, fields) as s_cur,
        InsertCursor(target, fields) as i_cur
    ):
        for row in as_dict(s_cur):
            for fuel_data, fuel_fields in conversion_map:
                new_row = fuel_data.copy()
                new_row.update({k: row.get(v, null_value) for k, v in fuel_fields.items()})
                i_cur.insertRow([new_row.get(f, null_value) for f in i_cur.fields])

Here we create a context block with two cursors, one to search through the origin table ("Forest_Surface_Fuels") and one to insert into the target table ("1_10_100"). 

  1. (line: 2) Create a context block with 2 cursors for the Target and Origin
  2. (line: 6) Iterate dictionary mappings of the origin rows
  3. (line: 7) Iterate all defined row conversions
  4. (line: 8 ) Create a copy of the template row data
  5. (line: 9) Get the field value of the current origin row for each mapped target field
  6. (line: 10) Create a row in the correct shape by iterating the insert fields and grabbing the new_row values

The null_value here shouldn't ever trigger, since it'll only exist if the fieldname doesn't exist in the origin table. If you don't have any 0 values, you can re-map nulls by using an or instead:

new_row.update({k: row.get(v) or null_value for k, v in fuel_fields.items()})

This abuses pythons truthiness, so any value that's considered falsey will get the null_value ('', 0, False, None, etc.)

Finally, we need to put all this together into a script that doesn't pollute the global namespace with cursor references. Basically just put your logic into an if __name__ == '__main__' block:

from collections.abc import Iterator
from typing import Any
from arcpy.da import SearchCursor, InsertCursor

ConversionMap = list[tuple[dict[str, Any], dict[str, Any]]]

def as_dict(cur: SearchCursor) -> Iterator[dict[str, Any]]:
    for row in cur:
        yield dict(zip(cur.fields, row))

def convert(origin: str, target: str, fields: list[str], conversion_map: ConversionMap, null_value: Any=None):
    with (
        SearchCursor(origin, fields) as s_cur,
        InsertCursor(target, fields) as i_cur
    ):
        for row in as_dict(s_cur):
            for fuel_data, fuel_fields in conversion_map:
                new_row = fuel_data.copy()
                new_row.update({k: row.get(v, null_value) for k, v in fuel_fields.items()})
                i_cur.insertRow([new_row.get(f, null_value) for f in i_cur.fields])

if __name__ == '__main__':
    # Fields that data will be inserted into in the final excel table
    FIELDS = ['Plot_', 'SurveyType', 'Transect', 'OneHour', 'TenHr', 'HunHr']
    
    ORIGIN_DATA = "Forest_Surface_Fuels"
    TARGET_DATA = "1_10_100"

    # Update this with the Null Value you want to use, e.g. ['' or -1 or None]
    NULL_VALUE = None

        # First Dictionary is direct data writes, second is field values to pull from the Origin dataset
    CONVERSION_MAP: ConversionMap = [
        (
            # Direct Data Insertion
            {'Plot_': 'Macroplot', 'SurveyType': 'Pre', 'Transect': '1'},
            # Field Value Map from Origin
            {'OneHour': 'onehr_pre_1', 'TenHr': 'tenhr_pre_1', 'HunHr': 'hundhr_pre_1'}
        ),
        (
            {'Plot_': 'Macroplot', 'SurveyType': 'Pre', 'Transect': '2'}, 
            {'OneHour': 'onehr_pre_2', 'TenHr': 'tenhr_pre_2', 'HunHr': 'hundhr_pre_2'}
        ),
        (
            {'Plot_': 'Macroplot', 'SurveyType': 'Post', 'Transect':'1'}, 
            {'OneHour': 'onehr_post_1', 'TenHr': 'tenhr_post_1', 'HunHr': 'hundhr_post_1'}
        ),
        (
            {'Plot_': 'Macroplot', 'SurveyType': 'Post', 'Transect':'2'}, 
            {'OneHour': 'onehr_post_2', 'TenHr': 'tenhr_post_2', 'HunHr': 'hundhr_post_2'}
        ),
    ]

    convert(ORIGIN_DATA, TARGET_DATA, FIELDS, CONVERSION_MAP, NULL_VALUE)

 

View solution in original post

5 Replies
VenkataKondepati
Regular Contributor

You’re very close, but the cursors are flipped and you’re passing the tuple list as “fields” (that’s why it blows up).

  • SearchCursor should read from Forest_Surface_Fuels (source)

  • InsertCursor should write to 1_10_100 (target)

  • InsertCursor("Forest_Surface_Fuels", fuelsTuple) is wrong because the 2nd argument must be a list of field names, not row data.

If your goal is: for each input feature, create 4 rows (Macroplot Pre/Post x Transect 1/2) and pull values from those named fields, do this pattern:

out_fields = ['Plot_', 'SurveyType', 'Transect', 'OneHour', 'TenHr', 'HunHr']

templates = [
    ("Pre","1", "onehr_pre_1","tenhr_pre_1","hundhr_pre_1"),
    ("Pre","2", "onehr_pre_2","tenhr_pre_2","hundhr_pre_2"),
    ("Post","1","onehr_post_1","tenhr_post_1","hundhr_post_1"),
    ("Post","2","onehr_post_2","tenhr_post_2","hundhr_post_2"),
]

# read Plot_ plus all fuel fields you reference
src_fields = ['Plot_'] + sorted({f for t in templates for f in t[2:]})

with arcpy.da.SearchCursor("Forest_Surface_Fuels", src_fields) as sc, \
     arcpy.da.InsertCursor("1_10_100", out_fields) as ic:

    idx = {name:i for i,name in enumerate(src_fields)}

    for r in sc:
        plot_id = r[idx['Plot_']]
        for survey, tran, f1, f10, f100 in templates:
            ic.insertRow([plot_id, survey, tran, r[idx[f1]], r[idx[f10]], r[idx[f100]]])

 

That gives you the “master table” (one row per plot per survey/transect) with 1/10/100 hr values populated correctly.

KevinDickinson1
Emerging Contributor

I keep getting an error, but want to confirm something first, when you define src_fields, where you have 'Plot_' that should be the corresponding field in "Forest_Surface_Fuels" to 'Plot_' in the out_fields, correct? I replaced 'Plot_' in those instances with 'Macroplot' and still get this error:

Screenshot 2025-12-28 093200.png

0 Kudos
VenkataKondepati
Regular Contributor

Yes — src_fields must be the field names in Forest_Surface_Fuels, not a value like "Macroplot". So don’t replace Plot_ with "Macroplot" in src_fields. "Macroplot" should be the value you insert, not a column name.

About the error you’re seeing:

RuntimeError: Invalid SQL syntax [near "PLOT": syntax error]

That usually happens when the target table has a field name that the underlying database treats as problematic (often a reserved word like PLOT, or a name that needs quoting). arcpy.da.InsertCursor builds an INSERT statement under the hood and can choke on that.

What to do:

  1. Confirm the exact field name in the output table (not the alias). If it’s PLOT / Plot / Plot_, verify spelling/case.

  2. Rename the output field to something safe like PlotID (best fix).

  3. Re-run the cursor after renaming.

So: keep src_fields as real source column names, and fix the output field name that’s triggering SQL near PLOT.

KevinDickinson1
Emerging Contributor

I ended up needing to convert Forest_Surface_Fuels  to a temporary layer for this to work, but it works as I hoped, so thank you!

HaydenWelch
MVP Regular Contributor

I'd approach this by making the field tuple a bit more complex. It seems that the first records are raw data and the last few are field values. You can just break it into two mappings like this:

# First Dictionary is direct data writes, second is field values to pull from the Origin dataset
conversion_map: list[tuple[dict[str, Any], dict[str, Any]]] = [
    (
        {'Plot_': 'Macroplot', 'SurveyType': 'Pre', 'Transect': '1'}, 
        {'OneHour': 'onehr_pre_1', 'TenHr': 'tenhr_pre_1', 'HunHr': 'hundhr_pre_1'}
    ),
    (
        {'Plot_': 'Macroplot', 'SurveyType': 'Pre', 'Transect': '2'}, 
        {'OneHour': 'onehr_pre_2', 'TenHr': 'tenhr_pre_2', 'HunHr': 'hundhr_pre_2'}
    ),
    (
        {'Plot_': 'Macroplot', 'SurveyType': 'Post', 'Transect':'1'}, 
        {'OneHour': 'onehr_post_1', 'TenHr': 'tenhr_post_1', 'HunHr': 'hundhr_post_1'}
    ),
    (
        {'Plot_': 'Macroplot', 'SurveyType': 'Post', 'Transect':'2'}, 
        {'OneHour': 'onehr_post_2', 'TenHr': 'tenhr_post_2', 'HunHr': 'hundhr_post_2'}
    ),
]

That will make it clearer later what the raw data mapping is and what the extracted field values are.

When dealing with multiple cursors, I tend to like mapping the rows instead of using an index. This can be done with this incredibly simple helper function:

def as_dict(cur: SearchCursor) -> Iterator[dict[str, Any]]:
    for row in cur:
        yield dict(zip(cur.fields, row))

You can wrap a cursor with this before iterating and you'll get back a dictionary that can be updated

 Now we need to implement the mapping:

def convert(origin: str, target: str, fields: list[str], conversion_map: ConversionMap, null_value: Any=None):
    with (
        SearchCursor(origin, fields) as s_cur,
        InsertCursor(target, fields) as i_cur
    ):
        for row in as_dict(s_cur):
            for fuel_data, fuel_fields in conversion_map:
                new_row = fuel_data.copy()
                new_row.update({k: row.get(v, null_value) for k, v in fuel_fields.items()})
                i_cur.insertRow([new_row.get(f, null_value) for f in i_cur.fields])

Here we create a context block with two cursors, one to search through the origin table ("Forest_Surface_Fuels") and one to insert into the target table ("1_10_100"). 

  1. (line: 2) Create a context block with 2 cursors for the Target and Origin
  2. (line: 6) Iterate dictionary mappings of the origin rows
  3. (line: 7) Iterate all defined row conversions
  4. (line: 8 ) Create a copy of the template row data
  5. (line: 9) Get the field value of the current origin row for each mapped target field
  6. (line: 10) Create a row in the correct shape by iterating the insert fields and grabbing the new_row values

The null_value here shouldn't ever trigger, since it'll only exist if the fieldname doesn't exist in the origin table. If you don't have any 0 values, you can re-map nulls by using an or instead:

new_row.update({k: row.get(v) or null_value for k, v in fuel_fields.items()})

This abuses pythons truthiness, so any value that's considered falsey will get the null_value ('', 0, False, None, etc.)

Finally, we need to put all this together into a script that doesn't pollute the global namespace with cursor references. Basically just put your logic into an if __name__ == '__main__' block:

from collections.abc import Iterator
from typing import Any
from arcpy.da import SearchCursor, InsertCursor

ConversionMap = list[tuple[dict[str, Any], dict[str, Any]]]

def as_dict(cur: SearchCursor) -> Iterator[dict[str, Any]]:
    for row in cur:
        yield dict(zip(cur.fields, row))

def convert(origin: str, target: str, fields: list[str], conversion_map: ConversionMap, null_value: Any=None):
    with (
        SearchCursor(origin, fields) as s_cur,
        InsertCursor(target, fields) as i_cur
    ):
        for row in as_dict(s_cur):
            for fuel_data, fuel_fields in conversion_map:
                new_row = fuel_data.copy()
                new_row.update({k: row.get(v, null_value) for k, v in fuel_fields.items()})
                i_cur.insertRow([new_row.get(f, null_value) for f in i_cur.fields])

if __name__ == '__main__':
    # Fields that data will be inserted into in the final excel table
    FIELDS = ['Plot_', 'SurveyType', 'Transect', 'OneHour', 'TenHr', 'HunHr']
    
    ORIGIN_DATA = "Forest_Surface_Fuels"
    TARGET_DATA = "1_10_100"

    # Update this with the Null Value you want to use, e.g. ['' or -1 or None]
    NULL_VALUE = None

        # First Dictionary is direct data writes, second is field values to pull from the Origin dataset
    CONVERSION_MAP: ConversionMap = [
        (
            # Direct Data Insertion
            {'Plot_': 'Macroplot', 'SurveyType': 'Pre', 'Transect': '1'},
            # Field Value Map from Origin
            {'OneHour': 'onehr_pre_1', 'TenHr': 'tenhr_pre_1', 'HunHr': 'hundhr_pre_1'}
        ),
        (
            {'Plot_': 'Macroplot', 'SurveyType': 'Pre', 'Transect': '2'}, 
            {'OneHour': 'onehr_pre_2', 'TenHr': 'tenhr_pre_2', 'HunHr': 'hundhr_pre_2'}
        ),
        (
            {'Plot_': 'Macroplot', 'SurveyType': 'Post', 'Transect':'1'}, 
            {'OneHour': 'onehr_post_1', 'TenHr': 'tenhr_post_1', 'HunHr': 'hundhr_post_1'}
        ),
        (
            {'Plot_': 'Macroplot', 'SurveyType': 'Post', 'Transect':'2'}, 
            {'OneHour': 'onehr_post_2', 'TenHr': 'tenhr_post_2', 'HunHr': 'hundhr_post_2'}
        ),
    ]

    convert(ORIGIN_DATA, TARGET_DATA, FIELDS, CONVERSION_MAP, NULL_VALUE)