Python: modify method-local variable inside inner method

Question:

I’m writing a test method called, say, test_foo (using pytest). I am testing the behavior of a function, foo, which takes as an argument another function, get. foo calls get iteratively, conditionally on the return value of get, e.g.:

def foo(get, param):
    max_num_tries = 3
    curr_num_tries = 0
    response = get(param)
    while curr_num_tries < max_num_tries and response.status_code == BAD_STATUS_CODE:
        response = get(param)
    return response

I am trying to override get such that it has access to how many times it’s been called and can return different values accordingly.

Here’s a simplified version of what I have so far:

def test_foo():
    tries_so_far = 0

    def get(arg1):
        global tries_so_far
        if tries_so_far < 3:
            tries_so_far += 1
            print("do something special here")
        else:
            print("do something else here")
        return "some return val"

    foo(get, "some arg")

However, I get the following error:

NameError: global name 'tries_so_far' is not defined

If I define tries_so_far outside of test_foo, at the module level, I get the expected behavior. However, I would like tries_so_far to be a variable that is local to test_foo.

Is there some way to give get read/write to tries_so_far using globals, or some other technique? Note: I cannot change the parameters or return value of get.

Asked By: LateCoder

||

Answers:

According to the accepted answer for this question Why can functions in Python print variables in enclosing scope but cannot use them in assignment?, there is an additional statement added in Python 3: nonlocal that does what you want. It is like global, but says to look at the enclosing scope instead of the module level. The following modification should therefore allow you to do exactly what you want:

def test_foo():
    tries_so_far = 0

    def get(arg1):
        nonlocal tries_so_far
        if tries_so_far < 3:
            tries_so_far += 1
            print("do something special here")
        else:
            print("do something else here")
        return "some return val"

    foo(get, "some arg")

While your question is not an exact duplicate of the one I cited above, you should read the accepted answer. It is very good and will probably address many unstated questions that you have on the subject.

Answered By: Mad Physicist