Why is my variable is unbound in one inner function but not the other?

Question:

In the code below, why does the first version of say work but the second version throws "local variable ‘running_high’ referenced before assignment"?

def announce_highest(who, last_score=0, running_high=0):
    
    assert who == 0 or who == 1, 'The who argument should indicate a player.'
    ''' this one works
    def say(*scores):
        assert len(scores) == 2
        gain = scores[who] - last_score 
        if gain > running_high:
            print(gain, "point(s)! That's the biggest gain yet for Player", who)
            return announce_highest(who, scores[who], gain)
        return announce_highest(who, scores[who], running_high)
    return say
    '''
    # this one errors "local variable 'running_high' referenced 
    # before assignment"
    def say(*scores): 
        gain = scores[who] - last_score
        if gain > running_high:
            running_high = scores[who]-last_score            
            print(gain,"point(s)! That's the biggest gain yet for Player",who) 
            return announce_highest(who, scores[who], gain)   
        return announce_highest(who,scores[who],running_high)
    return say
Asked By: Azerukit

||

Answers:

There’s one key difference between your two solutions which is causing them to behave differently: In the second solution, you assign a value to running_high on the third line:

    def say(*scores): 
        gain = scores[who] - last_score
        if gain > running_high:
            # here
            running_high = scores[who]-last_score   

Python variable scoping is a little bit different than other languages. The order of name resolution is as follows:

  1. Local (function) scope. There is no block scope in python, in contrast to many other languages, so e.g. the following code is valid:

     if True:
         foo = "foo"
     else:
         foo = "bar"
     print(foo)
    

    Even though foo is defined inside either the if block or the else block, it can be used outside of those blocks because it’s still in the local scope.

  2. Enclosing (nonlocal) scope. Inside say, running_high is in this scope, because it is a parameter to announce_highest, the enclosing function.

  3. Global (module) scope. The top-most scope accessible to the programmer. If you define something in a script outside of a function, it’s in the global scope.

  4. Built-in scope. This contains all of python’s built-in functions, keywords, etc.

Coming back to the question at hand, when you make an assignment to running_high in your second solution, you redefine it in the local scope. This is known as shadowing. We say the function-local running_high defined in say shadows the enclosing function running_high defined as a parameter of announce_highest.

However, the assignment on line 3 is not the first thing you do with your new local variable. On line 2, you try to use it in a comparison (if gain > running_high:). The interpreter hasn’t yet seen running_high at this point in the local scope and as such has no value for it, hence the error.

The solution to this is to use the special keywords global and nonlocal to tell the interpreter that running_high is not a local variable. In this specific case, running_high is in the enclosing or nonlocal scope, so you need to rewrite your second solution as follows:

    def say(*scores): 
        nonlocal running_high
        ...

To recap, here is your first solution:

    def say(*scores):
        assert len(scores) == 2
        gain = scores[who] - last_score 
        # running_high isn't in the local scope, so the interpreter
        # automatically looks for it in the enclosing scope. it exists
        # as a parameter of announce_highest, so this is valid code
        if gain > running_high:
            print(gain, "point(s)! That's the biggest gain yet for Player", who)
            return announce_highest(who, scores[who], gain)
        return announce_highest(who, scores[who], running_high)

And here is the second:

    def say(*scores): 
        gain = scores[who] - last_score
        if gain > running_high:
            # you didn't tell the interpreter where to look for the name
            # running_high, so it assumes you want to create a new local
            # variable. however, you tried to access running_high on the
            # previous line before it had a value, hence the error
            running_high = scores[who]-last_score            
            print(gain,"point(s)! That's the biggest gain yet for Player",who) 
            return announce_highest(who, scores[who], gain)   
        return announce_highest(who,scores[who],running_high)
Answered By: thisisrandy
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.