Python, are list and string variables actually scoped differently?

Viewed 47

I'm learning about scope in Python 3, and this example has me confused. Comparing behaviors of a list variable and a string variable when called inside a function:

foo1 = []
foo2 = ''
def f():
    foo1.append(3)
    global foo2
    foo2 += 'c'
    print('foo1-in-f:',foo1)
    print('foo2-in-f:',foo2)

print('foo1-before:',foo1)
print('foo2-before:',foo2)
f()
print('foo1-after:',foo1)
print('foo2-after:',foo2)

The output, as expected, is:

foo1-before: []
foo2-before:
foo1-in-f: [3]
foo2-in-f: c
foo1-after: [3]
foo2-after: c

I'm confused why the string must be declared global, as in the line global foo2, but the list is not declared global, as in there in no line global foo1.

I ran the code omitting the line global foo2 and unsurprisingly got UnboundedLocalError: local variable 'foo2' referenced before assignment. But, why am I not getting this error for foo1?

Any insights appreciated. I want to make sure I'm understanding how this really works.

2 Answers

In a Python function, all variable references are assumed to be global unless named locally. All new objects are created in local scope and there are restrictions on transferring or modifying objects to another scope.

You can do:

a=1
def f(): return a+1      # unnamed integer object created and returned

>>> f()
2

You can modify the contents of a global mutable variable since this does not assign a locally named object to the global scope:

ls=['string']
def f():
    ls.append('another')  # unnamed string object created and added to ls
    ls[0]+='_modified'    # ls[0] read, new object created with +=, 
                          # new object added to ls[0]   

>>> f()
>>> ls
['string_modified', 'another']

But it would be an error to use ls+=[something] since the assignment += is treated as ls being local in scope then reassigned to the global scope:

ls=[]
def f():
    ls+=['new entry'] # UnboundLocalError

The issue you are seeing is not necessarily modifying global variables. It is with reassigning the name that the function has used locally to the global scope.

There is a FAQ on this issue on the Python website.

There is an expanded FAQ on Eli Bendersky's blog.

You need to know how variable scopes work in Python. Python does not require you to declare variables, but assumes that a variable assigned in the body of a function is local. You can see this reflected by the compiler in the generated bytecode:

foo1 = []
foo2 = ''
def f():
    foo1.append(3)
    foo2 += 'c'

from dis import dis
dis(f)
  4           0 LOAD_GLOBAL              0 (foo1)
              2 LOAD_METHOD              1 (append)
              4 LOAD_CONST               1 (3)
              6 CALL_METHOD              1
              8 POP_TOP

  5          10 LOAD_FAST                0 (foo2)
             12 LOAD_CONST               2 ('c')
             14 INPLACE_ADD
             16 STORE_FAST               0 (foo2)
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

foo1 is loaded from the global context because foo1.append(3) is an item assignment operation - it modifies the actual reference. AFAIK Python strings are immutable which means that you need copy the value before doing the assignment, which is what you are doing inside the function because foo2 += 'c' will actually create a new string. Try running foo1 += [3] and you will get the same UnboundLocalError for foo1.

foo1.append(3) is an item assignment operation which is equivalent to foo1[len(foo1):] = [3]. This kind of operation is not possible for Python string for reasons stated above - try running foo2[:] = 'c' and you will get the error 'str' object does not support item assignment.

Now, the global keyword basically tells the interpreter to treat foo2 as a global variable in spite of the assignment within the function.

Related