May 15, 2011 at 05:43 Tags Articles , Python internals

If you're closely following the Python tag on StackOverflow, you'll notice that the same question comes up at least once a week. The question goes on like this:

x = 10 def foo (): x += 1 print x foo()

Why, when run, this results in the following error:

Traceback (most recent call last): File "unboundlocalerror.py", line 8, in <module> foo() File "unboundlocalerror.py", line 4, in foo x += 1 UnboundLocalError: local variable 'x' referenced before assignment

There are a few variations on this question, with the same core hiding underneath. Here's one:

lst = [ 1 , 2 , 3 ] def foo (): lst.append( 5 ) # OK #lst += [5] # ERROR here foo() print lst

Running the lst.append(5) statement successfully appends 5 to the list. However, substitute it for lst += [5] , and it raises UnboundLocalError , although at first sight it should accomplish the same.

Although this exact question is answered in Python's official FAQ (right here), I decided to write this article with the intent of giving a deeper explanation. It will start with a basic FAQ-level answer, which should satisfy one only wanting to know how to "solve the damn problem and move on". Then, I will dive deeper, looking at the formal definition of Python to understand what's going on. Finally, I'll take a look what happens behind the scenes in the implementation of CPython to cause this behavior.

The simple answer As mentioned above, this problem is covered in the Python FAQ. For completeness, I want to explain it here as well, quoting the FAQ when necessary. Let's take the first code snippet again: x = 10 def foo (): x += 1 print x foo() So where does the exception come from? Quoting the FAQ: This is because when you make an assignment to a variable in a scope, that variable becomes local to that scope and shadows any similarly named variable in the outer scope. But x += 1 is similar to x = x + 1 , so it should first read x , perform the addition and then assign back to x . As mentioned in the quote above, Python considers x a variable local to foo , so we have a problem - a variable is read (referenced) before it's been assigned. Python raises the UnboundLocalError exception in this case . So what do we do about this? The solution is very simple - Python has the global statement just for this purpose: x = 10 def foo (): global x x += 1 print x foo() This prints 11 , without any errors. The global statement tells Python that inside foo , x refers to the global variable x , even if it's assigned in foo . Actually, there is another variation on the question, for which the answer is a bit different. Consider this code: def external (): x = 10 def internal (): x += 1 print (x) internal() external() This kind of code may come up if you're into closures and other techniques that use Python's lexical scoping rules. The error this generates is the familiar UnboundLocalError . However, applying the "global fix": def external (): x = 10 def internal (): global x x += 1 print (x) internal() external() Doesn't help - another error is generated: NameError: global name 'x' is not defined . Python is right here - after all, there's no global variable named x , there's only an x in external . It may be not local to internal , but it's not global. So what can you do in this situation? If you're using Python 3, you have the nonlocal keyword. Replacing global by nonlocal in the last snippet makes everything work as expected. nonlocal is a new statement in Python 3, and there is no equivalent in Python 2 .

The formal answer Assignments in Python are used to bind names to values and to modify attributes or items of mutable objects. I could find two places in the Python (2.x) documentation where it's defined how an assignment to a local variable works. One is section 6.2 "Assignment statements" in the Simple Statements chapter of the language reference: Assignment of an object to a single target is recursively defined as follows. If the target is an identifier (name): If the name does not occur in a global statement in the current code block: the name is bound to the object in the current local namespace.

Otherwise: the name is bound to the object in the current global namespace. Another is section 4.1 "Naming and binding" of the Execution model chapter: If a name is bound in a block, it is a local variable of that block. [...] When a name is used in a code block, it is resolved using the nearest enclosing scope. [...] If the name refers to a local variable that has not been bound, a UnboundLocalError exception is raised. This is all clear, but still, another small doubt remains. All these rules apply to assignments of the form var = value which clearly bind var to value . But the code snippets we're having a problem with here have the += assignment. Shouldn't that just modify the bound value, without re-binding it? Well, no. += and its cousins ( -= , *= , etc.) are what Python calls "augmented assignment statements" [emphasis mine]: An augmented assignment evaluates the target (which, unlike normal assignment statements, cannot be an unpacking) and the expression list, performs the binary operation specific to the type of assignment on the two operands, and assigns the result to the original target. The target is only evaluated once. An augmented assignment expression like x += 1 can be rewritten as x = x + 1 to achieve a similar, but not exactly equal effect. In the augmented version, x is only evaluated once. Also, when possible, the actual operation is performed in-place, meaning that rather than creating a new object and assigning that to the target, the old object is modified instead. With the exception of assigning to tuples and multiple targets in a single statement, the assignment done by augmented assignment statements is handled the same way as normal assignments. Similarly, with the exception of the possible in-place behavior, the binary operation performed by augmented assignment is the same as the normal binary operations. So when earlier I said that x += 1 is similar to x = x + 1 , I wasn't telling all the truth, but it was accurate with respect to binding. Apart for possible optimization, += counts exactly as = when binding is considered. If you think carefully about it, it's unavoidable, because some types Python works with are immutable. Consider strings, for example: x = "abc" x += "def" The first line binds x to the value "abc". The second line doesn't modify the value "abc" to be "abcdef". Strings are immutable in Python. Rather, it creates the new value "abcdef" somewhere in memory, and re-binds x to it. This can be seen clearly when examining the object ID for x before and after the += : >>> x = "abc" >>> id (x) 11173824 >>> x += "def" >>> id (x) 32831648 >>> x 'abcdef' Note that some types in Python are mutable. For example, lists can actually be modified in-place: >>> y = [ 1 , 2 ] >>> id (y) 32413376 >>> y += [ 2 , 3 ] >>> id (y) 32413376 >>> y [ 1 , 2 , 2 , 3 ] id(y) didn't change after += , because the object y referenced was just modified. Still, Python re-bound y to the same object .