3.8.7 nonlocal referencing bound variable gets "SyntaxError no binding"

First post: Please forgive me if this is in the wrong subforum.

Module “M” has one function, “F”. Variable “r” is bound at the start of module “M”. A “nonlocal r” statement in function “F” produces the above “no binding” syntax error.

Motivation: I want “eval()” to evaluate a simple formula, e.g., “( r - 2) * n * n”. “r” and “n” are both set inside the function, but “eval()” fails to find “r” and uses a value of “n” assigned outside the function.

I want to make “r” visible to “eval()”. I can do it by using “global r”, but not by using “nonlocal”. What am I missing?

The “eval()” is used inside a list comprehension. Outside the comprehension, it works fine.

Sample code:

# Module M:
r = 1
def F( aEval):  
  nonlocal r
  eval( aEval)
F( 'r-2')

Code showing eval inside a list comprehension:

# Module M:

if True:

# r = 1
  def F( aEval=[]):  
  # nonlocal r
    def G():
      pass
    for tChar in 'abc':
      if tChar == 'b':
        r = 7  
        tEval = aEval[ 0]
        print( [ f'E({ eval( tEval)})' for n in range( 3)])  # List comprehension: eval now returns -1!

  F( ['r-2'])

Recall the difference between global versus nonlocal: nonlocal causes the name to refer to an enclosing scope other than the globals. List comprehensions have their own scope. This works:

r = 1
def F( aEval):  
  global r
  return eval( aEval)
print(F( 'r-2')) # -1

Side note: make sure you don’t eval anything containing user input you don’t trust.

Thank you!

Here is what surprises me: The list comprehension skips over the values bound in the function and jumps outside and retrieves values from the module! That’s counter-intuitive.

And the caveat restricting the use of nonlocal to “second-level scopes” – scopes within scopes within the global scope – also seems counter-intuitive or inconsistent. One of the main arguments for “nonlocal” in PEP 3104 is context independence: Putting a function definition inside another function should not affect the operation of the function. But it turns out that the depth of the scope does matter.

I’m just observing, not complaining: I love python!

Hi EarthBotV2,

You can’t use a nonlocal variable if there is not a nonlocal variable to
bind to.

This is a little complicated, so bear with me. Globals and nonlocals
behave slightly differently. When you declare a global, Python doesn’t
care whether it already exists or not.

But when you declare a nonlocal, it must already exist. That means that
nonlocal declarations can only work inside a nested function:

def outer():
    a = None  # The *local* variable a must exist.
    def inner():
        nonlocal a
        # nonlocal must be inside the inner function, not the
        # outer function
        ...

In fact, I’m surprised that you don’t get a syntax error saying that the
nonlocal declaration is illegal in a non-nested function.

The reason for this difference is a bit complicated, but basically,
global variables live inside a dict and can be created and destroyed
dynamically, but nonlocals live inside a closure and a storage slot must
be pre-allocated for them. And a closure is a snapshot of the
environment inside a running function, so if there is no running
function, there is no closure and you cannot have a nonlocal.

Anyway, the bottom line is that nonlocals can only be used inside nested
functions, and they always refer to a variable in one of the surrounding
functions.

Let’s put eval aside for now. Can you show us what you are trying to do
inside a comprehension, without assuming any mechanism, and we may be
able to suggest a way to do it that may or may not involve eval.

I’m afraid your sample code confuses me:

# Module M:

if True:

# r = 1
  def F( aEval=[]):  
  # nonlocal r
    def G():
      pass
    for tChar in 'abc':
      if tChar == 'b':
        r = 7  
        tEval = aEval[ 0]
        print( [ f'E({ eval( tEval)})' for n in range( 3)])  # List comprehension: eval now returns -1!

  F( ['r-2'])

Some questions:

  • Why do you have an unnecessary if True branch, since True is always
    true, the if block will always run;

  • Why do you pass the string you want to evaluate inside a list?

  • Why does the parameter have a default value that isn’t used?

  • Are you aware that default mutable arguments may cause confusion?

  • What is the purpose of the do-nothing G function?

  • What’s the purpose of the inner for-loop?

  • What’s E?

  • What’s the purpose of the f-string, instead of just calling eval
    directly?

I think that this might demonstrate your issue in a more straight-
forward way:

r = 1
def F(expression):
    t = 'b'
    r = 7  
    print([ eval(expression) for i in range(1) ])

F('r - 1')

Which prints [0], but I think you are expecting to print [6].
Correct?

Tell us what you are trying to do, and we’ll see what we can do to help.

Thank you for your lengthy and informative comment.

Nonlocal: You explained the purpose well, so my questions have been answered. You and D. Sweeney have helped me to understand the issue, and that is exactly what I needed.

Eval: I replaced the list comprehension with a simple two-line for, and that eliminated the problem. But I’m somewhat confused here.

I thought I could trust list comprehensions, but now I see that I need to be wary when the comprehension references a variable bound outside the comprehension. Will the comprehension see the variable in the function that uses the comprehension, or will it look in some other scope, and, if so, which scope?!

You asked me to explain the program.

  • (1) I left out most of the code and only posted relevant portions, with schematic names “F”, “G”, etc…
  • (2) The program generates multidimensional figurate numbers – triangular, tetrahedral, square, cubal, tesseractual, etc. – and compares the summation results with a given polynomial expansion.
  • (3) The “True” block is there so that I have a column available for “#” when I want to suppress a line of code. I often follow the “True” block with a “False” block, where I dump deleted code.
  • (4) In the full program, I pass a list of expressions, one for each dimension I want to test; I kept the list in the sample, because I wanted the sample to represent the logic in the original
  • (5) In the full program, I have a real embedded function – not actually called “G”! It receives a list of values for the current dimension and derives values for the next dimension.
  • (6) I use the f-string to format the eval results; the ‘E’ represents a label to be printed in front of the values, etc…
  • (7) The empty list after the “aEval=” was intended to suggest that “aEval” would be a list; it was quick-and-dirty “documentation”, and was not intended to serve as a default.

In conclusion, I need the “eval()” to reference the “r” (rank) bound inside the function. That happens when I call “eval()” in a “for” loop, but not when I call it within the comprehension. Is this really the way comprehensions are supposed to work, or is this a bug?

Interesting discussion!