-1

I have the following function:

def x():
    print(min(0, 1))
    min = 7
    print(min)

On the face of it (naively), it should print 0, then 7. In fact it raises an error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in x
UnboundLocalError: local variable 'min' referenced before assignment

How does defining min as a local variable in min = 7 prevent it from being used as the builtin before hand? Does Python build a list of local variables (something like __slots__ for a class) as it is compiling the function?

martineau
  • 119,623
  • 25
  • 170
  • 301
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • 2
    "Does Python build a list of local variables (something like `__slots__` for a class) as it is compiling the function?" - yup. – user2357112 Dec 07 '16 at 23:36
  • 1
    The fact that it is a builtin is a red-herring. Try that with any variable that is in the global scope and the same thing will happen. – juanpa.arrivillaga Dec 07 '16 at 23:40
  • @juanpa.arrivillaga I figured it applied to any global name. `min` was just the first thing that came to mind for a minimal example. My question is purely about why it happens. – Mad Physicist Dec 08 '16 at 03:17

2 Answers2

7

The compile phase for Python identifies all names that are assigned to within the scope of a function, and marks those names as locals (they're assigned an index in a local variable array in CPython, so using them doesn't involve dictionary lookups at all).

Being a local is all or nothing, for the entire scope of the method. You can't treat a variable as local for part of the method, and global/built-in for the rest. Per the language reference on naming and binding:

A scope defines the visibility of a name within a block. If a local variable is defined in a block, its scope includes that block.

Like most language standards, that's rather dry, but the important point is that defining a local variable within the block makes it local for the (implied) whole block, not from point of definition.

If you need to do something like this, you can make a local from the qualified name of the builtin initially, then change it later, e.g.:

import builtins  # On Python 2, __builtin__

def x():
    min = builtins.min
    print(min(0, 1))
    min = 7
    print(min)

Or you can use a cheesy hack based on compile-time default value caching for the same purpose:

def x(min=min): # Caches builtin function as a default value for local min
    print(min(0, 1))
    min = 7
    print(min)

If you're on Python 3, you'd want to do def x(*, min=min): to make it a keyword only argument, so it can't be overridden by the caller if they accidentally pass too many arguments positionally.

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
  • That's called the [`LEGB`](https://www.embrangler.com/2011/01/python-scoping-understading-legb/) manner. – Mazdak Dec 07 '16 at 23:43
  • 2
    @Kasramvd: LEGB defines the order of lookup, but it's not, in itself, describing the fact that "local" is for the entire block, not just from point of definition onward. – ShadowRanger Dec 07 '16 at 23:45
  • Indeed, but my comment has nothing to do with *definition onward*, once python encounters the `min` variable within its local namespace it doesn't continue the lookup. – Mazdak Dec 07 '16 at 23:48
  • 3
    @Kasramvd: True, but that's kind of subtle in itself. If a global that shadows a builtin name is defined at top level on line 2, and you use that name at top level on line 1 and line 3, you get the builtin on line 1 and the shadowing name on line 3. The fact that locals don't work that way while globals do makes references to LEGB less than useful here. Nested scope is even weirder, where calling the nested function in the outer function before locals in the outer scope have been defined and having the inner function print gets a value like `.bar at 0x7f5887930d08>`. – ShadowRanger Dec 07 '16 at 23:52
  • 1
    @ShadowRanger: I think you just screwed up your test with nested scopes, probably by [printing the nested `bar` function itself](http://ideone.com/Vhvhuo) instead of printing the unbound closure variable. If you try to access a closure variable before a value is assigned to it, [you get a NameError](http://ideone.com/h6Qwnw). – user2357112 Dec 08 '16 at 00:37
  • 1
    @user2357112: Yeah, you're right. My fault for trying to finish test and comment in a rush before I had to go help with kiddo bedtime rituals. So nested scopes are more like local scopes, while global scope is different. Either way, LEGB is not particularly helpful for understanding it. – ShadowRanger Dec 08 '16 at 01:08
3

Yes, specifically, if you take a look at the byte code generated, you'll see that Python tries to load it as a local with LOAD_FAST:

>>> dis(x)
  2           0 LOAD_GLOBAL              0 (print)
              3 LOAD_FAST                0 (min)
              6 LOAD_CONST               1 (0)
              9 LOAD_CONST               2 (1)
             12 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)

During compilation of the function object (I'm pretty sure it is during the construction of the symbol table), an assignment to min is detected and any look-ups for that name will be resolved in the local scope.

You can actually peek at the symbol table constructed and see how the symbol (name) min is classified:

>>> import symtable
>>> s="""
... def x():
...     print(min(0, 1))
...     min = 7
...     print(min)
... """
>>> t = symtable.symtable(s, '', compile_type='exec')
>>> i = t.get_children()[0].get_symbols()[0]
>>> print(i)
<symbol 'min'>
>>> i.is_local()
True

Haven't taken a good look into the symtable internals but there just might be some overriding here. Afterwards, during execution, this will flat out fail due to the fact that the assignment happens after the reference and no value can be found.

Dimitris Fasarakis Hilliard
  • 150,925
  • 31
  • 268
  • 253