2

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.

1
  • 1
    += is implemented as a mutation for mutable types, but a rebinding for immutable types. One takeaway is that a += b is not generally equivalent to a = a + b. (For strings it is though, as you observed). Commented Mar 23, 2021 at 12:44

2 Answers 2

2

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the clarifying example and additional links. I'll dig into the += operator. At first glance, I assume a += 1 is equivalent to a = a + 1. Also, your approach to wrap strings in a list is nifty. For instance, after stepping out of the function, I could 'concatenate' the full string like: " ".join(['Zapdos', 'used', 'Thunder.'])
1

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.

1 Comment

Thanks, your commentary on the dis readout was helpful! I'll be using this package in the future to look under the hood when confronted by confusing behaviors.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.