76

I have code like this (simplified):

def outer():
    ctr = 0

    def inner():
        ctr += 1

    inner()

But ctr causes an error:

Traceback (most recent call last):
  File "foo.py", line 9, in <module>
    outer()
  File "foo.py", line 7, in outer
    inner()
  File "foo.py", line 5, in inner
    ctr += 1
UnboundLocalError: local variable 'ctr' referenced before assignment

How can I fix this? I thought nested scopes would have allowed me to do this. I've tried with 'global', but it still doesn't work.

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
Thomas O
  • 6,026
  • 12
  • 42
  • 60
  • also look to this question: http://stackoverflow.com/questions/2516652/scoping-problem-in-recursive-closure – Ruggero Turra Apr 09 '10 at 18:01
  • Does this answer your question? [Python overwriting variables in nested functions](https://stackoverflow.com/questions/7935966/python-overwriting-variables-in-nested-functions) – user202729 Feb 01 '21 at 01:20

3 Answers3

98

If you're using Python 3, you can use the nonlocal statement to enable rebinding of a nonlocal name:

def outer():
    ctr = 0

    def inner():
        nonlocal ctr
        ctr += 1

    inner()

If you're using Python 2, which doesn't have nonlocal, you need to perform your incrementing without barename rebinding (by keeping the counter as an item or attribute of some barename, not as a barename itself). For example:

...
ctr = [0]

def inner():
    ctr[0] += 1
...

and of course use ctr[0] wherever you're using bare ctr now elsewhere.

Kevin
  • 74,910
  • 12
  • 133
  • 166
Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
  • 1
    This seems almost too like a 'hack'. I'll use it, but it seems like a limitation of Python 2.x. Guess I'll be using 3.x soon though. – Thomas O Apr 09 '10 at 17:56
  • 1
    Alex - thanks - this scoping issue was making function composition messy in 2.x – Ben Fitzgerald Aug 21 '12 at 15:43
  • Thanks ; I was going nuts trying to understand why I got an `UnboundLocalError` when I assigned the variable. Now that I see it, it actually makes sense. In C++, we have kind of the same concept with lambdas, except you declare the capturing before declaring the arguments, rather than in the body of the lambda. – adentinger Oct 23 '20 at 19:32
52

The Explanation

Whenever a value is assigned to a variable inside a function, python considers that variable a local variable of that function. (It doesn't even matter if the assignment is executed or not - as long as an assignment exists in a function, the variable being assigned to will be considered a local variable of that function.) Since the statement ctr += 1 includes an assignment to ctr, python thinks that ctr is local to the inner function. Consequently, it never even tries to look at the value of the ctr variable that's been defined in outer. What python sees is essentially this:

def inner():
    ctr = ctr + 1

And I think we can all agree that this code would cause an error, since ctr is being accessed before it has been defined.

(See also the docs or this question for more details about how python decides the scope of a variable.)

The Solution (in python 3)

Python 3 has introduced the nonlocal statement, which works much like the global statement, but lets us access variables of the surrounding function (rather than global variables). Simply add nonlocal ctr at the top of the innerfunction and the problem will go away:

def outer():
    ctr = 0

    def inner():
        nonlocal ctr
        ctr += 1

    inner()

The Workaround (in python 2)

Since the nonlocal statement doesn't exist in python 2, we have to be crafty. There are two easy workarounds:

  • Removing all assignments to ctr

    Since python only considers ctr a local variable because there's an assignment to that variable, the problem will go away if we remove all assignments to the name ctr. But how can we change the value of the variable without assigning to it? Easy: We wrap the variable in a mutable object, like a list. Then we can modify that list without ever assigning a value to the name ctr:

    def outer():
        ctr = [0]
    
        def inner():
            ctr[0] += 1
    
        inner()
    
  • Passing ctr as an argument to inner

    def outer():
        ctr = 0
    
        def inner(ctr):
            ctr += 1
            return ctr
    
        ctr = inner(ctr)
    
Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • 6
    I always appreciate answers that explain a specific behavior instead of just putting a piece of code. +1 – CentAu Aug 15 '19 at 22:42
  • The local scoping behaviour of inner functions you explained also occurs when an outer variable is used in a conditional statement in an inner function, not just during variable assignments. – Sean Francis N. Ballais Nov 10 '19 at 08:55
  • @SeanFrancisN.Ballais Not true. The only thing that makes a variable local is a [name binding operation](https://docs.python.org/3/reference/executionmodel.html#binding-of-names), and a conditional isn't one of them. – Aran-Fey Nov 25 '19 at 20:40
  • Passing as an argument doesn’t work if it’s a string, though‽ – mirabilos Dec 21 '21 at 00:34
-2

How about declaring ctr outside of outer (i.e. in the global scope), or any other class/function? This will make the variable accessible and writable.

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
Luiz C.
  • 746
  • 11
  • 22
  • 4
    Yes, but it's a bit messy having global variables just for one or two functions - better to keep them in local scopes so you know why you put them there in the first place. – Thomas O Apr 10 '10 at 01:28