0

Today I found some weird behavior of following piece of code:

if (arg == 0):
   # some local variable
   format = ""
   ret = format + arg
else:
   # bultin format function
   ret = format(arg, "#x")

print ret

It acts different inside and outside function. With this code:

import sys

def foo(arg):
   if (arg == 0):
      # some local variable
      format = ""
      ret = format + "0"
   else:
      # bultin format function
      ret = format(arg, "#x")

   print ret


arg = int(sys.argv[1])


print "Outside function:"
if (arg == 0):
   # some local variable
   format = ""
   ret = format + "0"
else:
   # bultin format function
   ret = format(arg, "#x")

print ret


print "Foo call:"
foo(arg)

I get following output of call: python format.py 1

Outside function:
0x1
Foo call:
Traceback (most recent call last):
  File "format.py", line 31, in <module>
     foo(arg)
  File "format.py", line 10, in foo
    ret = format(arg, "#x")

The first question is why a local variable under if statement hides the format function used in else statement?

The second one is why it acts different (and now with the expected behavior) when called outside a function?

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
woockashek
  • 1,588
  • 10
  • 25
  • Use [```str.format()```](https://docs.python.org/2/library/stdtypes.html#str.format) instead of [```format()```](https://docs.python.org/2/library/string.html#string.Formatter.format). – pzp Apr 02 '15 at 13:22
  • 1
    It is bad practice to create a variable with the same name as a built-in function anyway; one should never do this. But If you really want to know why it happens, Martin's answer is a very good explanation. – pzp Apr 02 '15 at 13:31

1 Answers1

3

Python distinguishes between globals and locals; outside the function, format is a global, inside the function, format is a local because you assign to it (if you never bound the name anywhere in the function, it'll be treated as a global instead). You cannot treat a name as both global and local at the same time.

The if doesn't matter here; name visibility applies to the whole scope; if doesn't introduce a new scope, only the function does. So within the function, format is a local name, and can never be looked up as a global, regardless of if blocks.

Outside the function your code works because there already is a global called format; it is a built-in function. If args == 0 is true, your code will work just once as afterwards it'll have rebound the global to a string and future calls to format() will fail.

Inside the function, format is now a local and never set if args != 0; it doesn't matter that the assignment is guarded by if here, it is either always a local or always a global, as this is determined at compile time.

You can easily avoid this problem by not overloading the name format here. You don't want to accidentally mask the name format in the first place as that can break other code that wants to use the format() function. For your specific example, it is trivial to just remove the use of format altogether:

def foo(arg):
    if (arg == 0):
        ret = "0"
    else:
        # bultin format function
        ret = format(arg, "#x")

   print ret

You could even just alter the formatting configuration based on the value of args:

def foo(arg):
    return format(arg, "#x" if args else "d")
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • @woockashek: but that means the compiler has detected it is a local. You cannot treat it as a global and a local based on dynamic choices here. – Martijn Pieters Apr 02 '15 at 13:20
  • I don't understand your explanation. I'm assigning a value to format only under the scope of if. I added `print locals()` in else statement and there is only `args` variable so the global `format()` function should not be hidden by a local variable. Moreover you can delete the call outside the function, so remove any assignment to a global variable and the behavior is still the same. – woockashek Apr 02 '15 at 13:23
  • @woockashek: that's what I am saying; `format` cannot be looked up as a global, **ever**, because your function **might** use it as a local. In Python, that means it is **always** a local in that function. – Martijn Pieters Apr 02 '15 at 13:26
  • 1
    @woockashek the clauses of an `if` statement *don't* have their own scope in Python. See [Short Description of Python Scoping Rules](http://stackoverflow.com/questions/291978/short-description-of-python-scoping-rules) – Lukas Graf Apr 02 '15 at 13:27
  • @woockashek: if you delete a global, you get a `NameError` (you can delete built-in names too by removing them from the [`builtins` module](https://docs.python.org/3/library/builtins.html)). – Martijn Pieters Apr 02 '15 at 13:27
  • @woockashek: but specifically: `if` doesn't introduce a new scope; only the function has a scope. Within a whole scope, a name is either local or taken from a surrounding scope. It can never be both local and coming from a surrounding scope. – Martijn Pieters Apr 02 '15 at 13:29
  • @Martijn: Ok, now I get the point. I fixed the example by calling explicitly `global format` under else statement. Thanks a lot! – woockashek Apr 02 '15 at 13:31
  • 1
    @woockashek Don't use ```global```. It creates a maintainability nightmare. Just name your local variable ```format``` something else. – pzp Apr 02 '15 at 13:33
  • @pzp1997: yeah I did this (so change the var name) right after getting this error. I just wanted to know the real cause of the problem. – woockashek Apr 02 '15 at 13:37