Python's Scoping Rules The extraordinarily short guide to Python's crazy scoping rules

· by

Contents

It’s obvious that you cannot access a variable before it was declared. But if it was declared inside a loop, can you access it outside a loop? If it was declared in a function, can you access the variable outside of the function?

This kind of “variable lifetime” is known as scoping. After reading this article, you will know the scoping rules of Python. Let’s start!

The 3 Scopes of Python

Python has 3 scopes:

  • Global: In the main part of the script. By default, this already contains the built-ins. You can access all global variables with globals()
  • Enclosed: In the outer function, if this is a nested function
  • Local: Within the current function. You can access all local variables with locals(). Within the main script, locals() == globals()

You can see all three in action here:

print(min([1, 2, 3]))


def foo():
    min = lambda n: "enclosing"

    def bar():
        """Bar is enclosed by 'foo'"""
        print(min([1, 2, 3]))

    def baz():
        """Baz is also enclosed by 'foo'"""
        min = lambda n: "local"
        print(min([1, 2, 3]))

    bar()
    baz()


min = lambda n: "global"
print(min([1, 2, 3]))

foo()

There is a crucial point here:

The scope of a variable is defined at compile-time!

For this reason, the following code throws an exception:

def foo():
    print(min([1, 2, 3]))
    min = lambda n: "local"


foo()

throws the exception:

Traceback (most recent call last):
  File "example.py", line 5, in <module>
    foo()
  File "example.py", line 2, in foo
    print(min([1, 2, 3]))
UnboundLocalError: local variable 'min' referenced before assignment

The “global” statement

You can assign a value to a global variable in a function with the global statement:

x = "global"


def foo():
    global x
    x = "local"


foo()
print(x)  # gives "local"

I need this very rarely. To make sure that I don’t confuse anything, I like to use the globals() dictionary. In this case, I would rather use:

x = "global"


def foo():
    globals()["x"] = "local"


foo()
print(x)  # gives "local"

The “nonlocal” statement

You can assign a value to an enclosed variable with the nonlocal statement:

x = "global"


def foo():
    x = "enclosed"

    def bar():
        nonlocal x
        x = "local"

    bar()
    print(x)  # gives "local"


foo()
print(x)  # gives "global"

Confusing Examples

Append element vs ‘+= [element]’

xs = []


def foo():
    xs.append(42)  # OK


foo()

vs

xs = []


def foo():
    xs += [42]  # UnboundLocalError: local variable 'xs'
    # referenced before assignment


foo()

The reason why the first works but not the second is that the first one calls a method of xs. It never assigns a value to xs. The second one is equivalent to:

xs = []


def foo():
    xs = xs + [42]


foo()

When Python parses the assignment xs = ..., xs is assigned to the local scope. But in the local scope, xs does not exist before xs = xs + [42] is executed. Hence the error.

In the first example with xs.append(42), the global xs is used. Hence, we don’t face any issue because it is defined in the global scope.

Global scope DOES fall back to built-ins

# prints 1
print(min([1, 2, 3]))
min = lambda n: "local"

but the same does not work in a local scope:

# UnboundLocalError: local variable 'min' referenced before assignment
def foo():
    print(min([1, 2, 3]))
    min = lambda n: "local"


foo()

The reason is that locals() == globals() within the global scope. Although built-ins are a bit special, they kind of live in the global scope.

Assignment

This one is confusing to people with a Java, C, or C++ background. This is valid Python code:

for x in range(10):
    y = 12
print(y)  # prints 12

But in Java, you need to declare it upfront to be able to use the variable after the loop:

public class Main
{
 public static void main(String[] args) {
     int y = 0;
     for (int i=0; i < 10; i++) {
         y = 12;
     }
  System.out.println(y);
 }
}

mypy

mypy is a widespread type-checker for Python.

if external_service():
    y = 42
else:
    y = "foo"
    # error: Incompatible types in assignment (expression has type "str", variable has type "int")

So mypy doesn’t like that you assign the string “foo” to y, because it first read the bool(external_service()) == True path and assumed that y would be an integer.

Then you might want to do this:

if external_service():
    y: int = 42
else:
    y: str = "foo"
    # error: Name 'y' already defined on line 2

You can see that mypy assumes that y should be the same type in both cases. It’s reasonable because otherwise the following analysis might get extremely complicated.

The next try might be:

y: Union[str, int] = None
# error: Incompatible types in assignment
# (expression has type "None", variable has type "Union[str, int]")

if external_service():
    y = 42
else:
    y = "foo"

That should have been expected. Assigning None to a type which does not include None is not possible — and adding None to that type might cause many more issues down the road.

You can do this:

if external_service():
    y: Union[str, int] = 42
else:
    y = "foo"

But I can also see why this feels strange. What might be cleaner is this:

y: Union[str, int]
if external_service():
    y = 42
else:
    y = "foo"

Summary

We have seen the three types of scopes in Python: local, enclosed, and global. We’ve seen that you can access globals with the global keyword or via the globals() dictionary. Locals can be accessed with the locals() dictionary and enclosed variables with the nonlocal keyword. Keep that in mind, and Python scoping should make sense 🙂