ArcPy CalculateField_management Complex Python Expression to Return a String
My coworker was having some trouble implementing a CalculateField expression in Python so I wanted to document the solution for anyone with a similar issue. (Note: If you just want the answer, see the What ultimately worked.. heading, below.)
Being unfamiliar with ArcPy, I was intrigued by this idea of defining a function as a string value, then passing that string as a variable into another function. I can see how that opens a powerful door—but it also strikes me as ultra weird, and because Python has some very particular spacing/indentation rules (as compared to say, JavaScript), I figured this technique would be ultra-prone to syntax issues ..and thus could be especially difficult to troubleshoot. So in this case, I also wanted to demonstrate how I ultimately thought through the problem.
After some quick Googling, we found two relatively useful pages of documentation in the ESRI support ecosystem, and both pages demonstrated different ways to formulate the function-as-string
parameter necessary for the CalculateField_management()
function.
The biggest difference between these examples is how they portrayed line breaks in the function-as-string
; we’ll consider them first.
The first example..
In the first ESRI example, the function-as-string
was concatenated together across multiple lines using a backslash \
like this..
codeblock = "def getclass(area):\
if area <= 1000:\
return 1\
if area > 1000 and area <= 10000:\
return 2\
else:\
return 3"
print codeblock
Realizing Python is very picky about syntax, I wanted to see what happened if I print codeblock
to the console–notice that spacing is preserved, but there are no line breaks:
Frankly, it’s difficult for me to believe this was ever a working example, and surprise—this approach didn’t end up working for us..
And the second example..
In the second ESRI example, the function-as-string
was concatenated together across multiple lines using triple-quotes """
to declare a multiline string, like this..
codeblock = """def getClass(area):
if area <= 1000:
return 1
if area > 1000 and area <= 10000:
return 2
else:
return 3"""
print codeblock
Similarly, I wanted to codeblock
to the terminal to see how the string was formatted before going into the ArcPy function; this time, the linebreaks were preserved:
..that looks more like a Python function, so that was a good start. But things still weren’t working..
How to test our custom function..?
We were still getting errors, and the stack dump claimed they were syntax errors. But because of the unusual nature of this software design, I wasn’t sure if they were really syntax errors, or some kind of misleading, catch-all error ..perhaps our code-in-a-string
function was flawed?
So my next step was to test our function, as a legit Python function, to see if it would accept and return a value like I expected.
def getClass(area): if area <= 1000: return 1 if area > 1000 and area <= 10000: return 2 else: return 3 print getClass(1500)
As you can see, the code ran without any syntax errors..
Also, 2 is the correct return value for the input we used in the test. So I felt comfortable that the function itself—as written—worked. So now I was perplexed as to why the ESRI CalculateField_management()
function was rejecting it ..with a syntax error.
What ultimately worked..
My co-worker’s function had another gotcha—unlike the two ESRI examples, which return numeric values, we needed our function to return a string. My Python was rusty, and I was forgetting if a string could be declared equally between single quotes i.e. 'MyString'
and double quotes, i.e.
"MyString"
. Plus, I still new we were looking for an alleged syntax error.
I finally Googled a third page in the ESRI documentation and noticed this little tidbit in some ESRI documentation:
Python enforces indentation as part of the syntax. Use two or four spaces to define each logical level. Align the beginning and end of statement blocks, and be consistent.
My coworker had originally used one and two spaces, respectively, to control indentation in his custom function, along with the first technique for declaring a multiline string. So I finally rewrote our function using the triple-quotes technique as well as 2 and 4 space indents like ESRI said to, and this is what we ended up with..
For the record, while a large
if
/elseif
block may not be the best way to write this function, it is a very readable way to write this function, not to mention the most intuitive from my coworker’s perspective.
codeblock = """def getclass(SPEEDLIM): if SPEEDLIM >= 60: return 'A10' elif SPEEDLIM == 55: return 'A30' elif SPEEDLIM == 50: return 'A20' elif SPEEDLIM == 45: return 'A30' elif SPEEDLIM == 40: return 'A30' elif SPEEDLIM == 35: return 'A40' elif SPEEDLIM == 30: return 'A00' elif SPEEDLIM == 20: return 'A00' elif SPEEDLIM == 15: return 'A00' elif SPEEDLIM == 10: return 'A61' elif SPEEDLIM == 5: return 'A62' elif SPEEDLIM <= 5: return 'A71'"""
And this worked. I think the fix resulted from using triple-quotes to declare the function string, rather than the backslash approach we started with. While we also changed the indentation to use 2/4-spaces, as mentioned above, it’s possible we unwittingly fixed a different syntax error in the function along the way. Since we didn’t make these changes independently, it’s impossible to know which exactly made the difference.
Summary
So this is a summary that might help you if you’re having difficulty with this kind of ArcPy data management exercise:
- Try writing a basic version of your function in a throw-away .py file and test it to make sure it definitely works—if nothing else, this is a good sanity check.
- Make sure to use the triple-quote technique to properly preserve line breaks, and declare your function as a string variable that you can pass into your call to the geoprocessor. (You may need to use single-quotes to declare any strings within your larger, multiline string, which was the technique we used.)
- If things still aren’t working, try using increments of 2 spaces to control various levels of indentation within your custom function, i.e. 2/4/6/8/etc..
Hopefully that helps you get where you’re trying to go.
Baaaaaaaaaaaaaaaaaaaaaaaaaaammmmm!!!!
– elrobis