Select to view content in your preferred language

Using Append in Python is suddenly requiring an edit session?

1310
11
02-11-2026 09:12 AM
Sean_Gambrel
Occasional Contributor

Hello,  A colleague and I have developed some python that, among other things, joins a table to a feature class and then copies specific features from there into a different, existing feature class.  This has run smoothly for over a year.  Suddenly the script is failing, stating that an edit session is needed to run the Append tool - which doesn't seem right - and anyway, our attempts at adding an edit session into the script causes the whole thing to fail from the get-go. 

A month or so ago there was a recent change to the database that the table comes from, but we've accommodated that change.  Nothing has changed on the gis-side in any way - feature classes, locations, etc.  Nothing has changed within our organizations enterprise / software that should affect this as far as I'm aware.  Any idea what's going on and how to repair this issue?   

I can't attach the full error message but the most pertinent part seems to be this:  "arcgisscripting.ExecuteError:  ERROR 160250: Objects in this class cannot be updated outside an edit session.  Failed to execute (Append)"

Here's what I think is the pertinent section of code, though it hasn't been changed:

...

survey_plat = r'V:\\gislu\\_DatabaseConnection\\editgis1.lu.user.dc.sde\\lu.LU.SURV_SurveyPlat'
count_pre_append = int(arcpy.management.GetCount(survey_plat)[0])

results = arcpy.management.Append(
inputs=surveys_added,
target=survey_plat,
schema_type='NO_TEST',
field_mapping="SURVEY_ID \"SURVEY_ID\" true true false 12 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,survey_id,0,7999;SURVEY_TYP \"SURVEY_TYP\" true true false 25 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,survey_type,0,7999;SURVEY_DES \"SURVEY_DES\" true true false 254 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,survey_descr,0,7999;SURVEY_YR \"SURVEY_YR\" true true false 4 Long 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,survey_year,-1,-1;PLSS_COORD \"PLSS_COORD\" true true false 2 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,PLSS_COORD,0,7999;LINK \"LINK\" true true false 256 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,link,0,7999;PARCEL_NO \"PARCEL_NO\" true true false 12 Text 0 0,First,#,V:\\prjlu\\SurveyPlats\\PlatProcessingAutomation\\SurveyProcessing.gdb\\SurveysAdded,parcel_id,0,7999"
)
print(results)
print(arcpy.GetMessages())

count_post_append = int(arcpy.management.GetCount(survey_plat)[0])
count_delta = count_post_append - count_pre_append
print(f'{count_delta} records appended')

Tags (1)
0 Kudos
11 Replies
MichaelVolz
Esteemed Contributor

Can you print out count_pre_append to see if the script is evaluating that variable correctly?

Maybe its a misleading error message, so I would put in additional print statements to make sure variables are being evaluated correctly.

Sean_Gambrel
Occasional Contributor

We added this and confirmed all looks well until the failure point - but then the script fails at the same point so I think the message is valid. 

0 Kudos
AlfredBaldenweck
MVP Frequent Contributor

This isn't super helpful, but can confirm stuff like this happens. I had a script that worked just fine on day 1 and then on day 2 required an edit session when I was using an update cursor. Hadn't even changed it yet.

TonyAlmeida
MVP Regular Contributor

Did something changed on the database side? Register As Versioned or enabled branch versioning. This has happened to me for no reason as well, but I also know if something on the database has changed it can trig it.

Try adding,

workspace = r"V:\gislu\_DatabaseConnection\editgis1.lu.user.dc.sde"

edit = arcpy.da.Editor(workspace)
edit.startEditing(False, True)
edit.startOperation()

arcpy.management.Append(
    inputs=surveys_added,
    target=survey_plat,
    schema_type="NO_TEST",
    field_mapping=...
)

edit.stopOperation()
edit.stopEditing(True)

  

Sean_Gambrel
Occasional Contributor

Thanks!  Gave this a shot but no change in behavior.  I think we may have a solution below though...  

HaydenWelch
MVP Regular Contributor

The Append function will hook into existing edit sessions if run in a map context. That behavior can be a bit odd when you then run the same tool with no active edit session

It's best practice to use the `with arcpy.da.Editor` context block when doing any write operations since it'll prevent undefined behavior when an edit session can't be found, and will fail gracefully if there's an issue during the transaction/edit block.

Additionally, using an edit context with arcpy.da.UpdateCursor can be a lot more informative since Append just kinda consumes a lot of row level messages. doing it with UpdateCursor lets you handle bad data (e.g. a single input row that has a text field of length of 1000 being appended to a target field of length 255).

Some others mentioned how to use the edit session here, but I would absolutely make sure to use the `with Editor as edit` pattern, since an exception being raised before editor.stopEditing is called will leave your database with an open edit lock.

 

Here's your script using Edit sessions and an InsertCursor

from arcpy.da import InsertCursor, SearchCursor, Editor

# Target Workspace
gdb = r'V:\prjlu\SurveyPlats\PlatProcessingAutomation\SurveyProcessing.gdb'
new_survey = 'SurveysAdded'

sde = r'V:\gislu\_DatabaseConnection\editgis1.lu.user.dc.sde'
survey_plat = 'lu.LU.SURV_SurveyPlat'

# Map new_survey -> survey_plat
field_map = {
    'SURVEY_ID': 'survey_id',
    'SURVEY_TYP': 'survey_typ',
    'SURVEY_DES': 'survey_descr',
    'SURVEY_YR': 'survey_year',
    'PLSS_COORD': 'PLSS_COORD',
    'LINK': 'link',
    'PARCEL_NO': 'parcel_id',
}

# Use these in the cursors
source_fields = list(field_map.keys())
target_fields = list(field_map.values())

new_rows = list(SearchCursor(new_survey, source_fields))

# You can use multiple context managers in one with block
with Editor(sde), InsertCursor(survey_plat, target_fields) as cur:
    for row in new_rows:
        cur.insertRow(row)
print(f'{len(new_rows)} records appended')

 

AlfredBaldenweck
MVP Frequent Contributor

Another reason to use an update cursor is that it's MILES faster than Append().

HaydenWelch
MVP Regular Contributor

Additionally, this style plays way better with version control since the field map isn't just a giant one line string that's hundreds of characters long.

Now you just update the mapping dictionary and can see exactly what changed with a commit.

 

Edit: Here's a more robust version of the above code that supports arbitrary mappings using lambda expressions (You can also just define normal functions and use those. That's a much better idea):

from collections.abc import Callable, Generator, Sequence
from typing import Any
from arcpy.da import InsertCursor, SearchCursor, Editor


type FieldCalc = Callable[[tuple[Any, ...]], Any]
type FieldNames = Sequence[str]
type FieldMap = dict[str, tuple[FieldCalc, FieldNames] | str]


def process_map(f_map: FieldMap) -> tuple[list[str], list[str]]:
    """Consume a field mapping dictionary and return all fields participating on both sides
    
    Note: Returned values are in order (source, target)
    """
    target_fields = list(f_map.keys())
    source_fields: list[str] = []
    for mapping in f_map.values():
        if isinstance(mapping, tuple):
            # mapping has a functional component
            _, fields = mapping
            source_fields.extend(fields)
        else:
            # mapping is direct
            source_fields.append(mapping)
    return source_fields, target_fields


# Map the row values to field names
def as_dict(cur: SearchCursor) -> Generator[dict[str, Any]]:
    """Make cursor rows dictionaries"""
    for row in cur:
        yield dict(zip(cur.fields, row))


def calculate_row(target_fields: Sequence[str], source_row: dict[str, Any], field_map: FieldMap) -> list[Any]:
    """Apply a Row-Wise functional/direct map to a Cursor
    
    Note: Returned list will match the field order supplied by `target_fields`
    """
    row_vals: list[Any] = []
    for field in target_fields:
        mapping = field_map.get(field)
        if mapping is None:
            raise ValueError(f'Field {field} is not mapped!')
        if isinstance(mapping, tuple):
            # Functional Map
            func, source_fields = mapping
            row_vals.append(func(tuple(source_row[f] for f in source_fields)))
        else:
            # Direct Map
            row_vals.append(source_row[mapping])
    return row_vals

def main():
    # Target Workspace
    new_survey = 'SurveysAdded'

    sde = r'V:\gislu\_DatabaseConnection\editgis1.lu.user.dc.sde'
    survey_plat = 'lu.LU.SURV_SurveyPlat'
    field_map: FieldMap = {
        'survey_id': (lambda vs: ';'.join(map(str, vs)), ['SURVEY_ID1', 'SURVEY_ID2']),
        'survey_typ': (lambda vs: ([v for v in vs if v]+[None]).pop(0), ['SURVEY_TYP', 'ALT_TYP']),
        'survey_descr': 'SURVEY_DES',
        'survey_year': 'SURVEY_YR',
        'PLSS_COORD': (lambda vs: vs[0][:6], ['PLSS_COORD']), # Truncate
        'link': (lambda vs: str(vs[0]).replace('http', 'https'), ['LINK']),
        'parcel_id': 'PARCEL_NO',
    }
    source_fields, target_fields = process_map(field_map)
    new_rows = list(as_dict(SearchCursor(new_survey, source_fields)))
    
    # You can use multiple context managers in one with block
    with Editor(sde), InsertCursor(survey_plat, target_fields) as cur:
        for row in new_rows:
            cur.insertRow(calculate_row(target_fields, row, field_map))
    print(f'{len(new_rows)} records appended')
    
    
if __name__ == '__main__':
    main()

 

Edit 2:

Here's a more reasonable way to do the functional mappings:

...

def join(vals: tuple[Any, ...]) -> str:
    return ';'.join(map(str, vals))

def first(vals: tuple[Any, ...]) -> Any:
    return next(filter(bool, vals), None)

def truncate(vals: tuple[str]) -> str:
    return vals[0][:6]

def make_https(vals: tuple[str]) -> str:
    return vals[0].replace('http', 'https')

field_map: FieldMap = {
    'survey_id': (join, ['SURVEY_ID1', 'SURVEY_ID2']),
    'survey_typ': (first, ['SURVEY_TYP', 'ALT_TYP']),
    'survey_descr': 'SURVEY_DES',
    'survey_year': 'SURVEY_YR',
    'PLSS_COORD': (truncate, ['PLSS_COORD']), # Truncate
    'link': (make_https, ['LINK']),
    'parcel_id': 'PARCEL_NO',
}

...
HaydenWelch
MVP Regular Contributor

@Sean_Gambrel did any of this work for you?

0 Kudos