Scopes and Global Variables

In this article we will discuss two python keywords - global and nonlocal - and scoping. Surprisingly, this topic was very hard to research and to wrap my head around. So, this post is, as always, a mix of how I understand the topic and what I was able to find online and verify (no ChatGPT-only answers). The focus here was to cover the most important and veryfiable scenarios and behaviors of the language.

Note: Cells in this notebook must be evaluated individually, meaning the kernel must be restarted before each cell is run. Otherwise name collisions may occur. In particular, some demonstrations depend on a global variable not being defined, but it may have been defined in a previous cell, ruining the demonstration. So, restart kernel before running each cell in this notebook.

Terminology

A scope is a block of code in which variables can be visible. A namespace is a table that defines variables (a mapping from names to pointers). Each scope has its own namespace.

A global scope is the one at the level of a module. This is also the level that your IPython shell is in when you start a new one. Any variable you define at this level is a global variable.

Inside this scope you can create an inner scope, for example by defining a function. And now things start to be interesting. Let’s assume we are in the scope of this function. Now, the current scope is called a local scope and the outer scope (which also happens to be the global scope in this case) is called a nonlocal scope.

This terminology holds for any number of nested scopes: The current scope is always local, the closest higher scope is nonlocal and the top-most scope is global.

Types of Scopes and LEGB

There is one more scope above global - the built-in scope. The full list of scopes in Python is this:

  • built-in
  • global
  • nonlocal (recursive)
  • local

This means that when a variable is referenced in a local scope, Python first looks for it in the local scope. If it isn’t found there the nearest enclosing scope is searched (the nonlocal scope). If it is not found there then the next higher scope is searched and so on, until a global scope is reached. If a variable is not found in the global scope, the built-in scope is searched. If the variable is not even here, a NameError is raised. This lookup process is called the LEGB rule - from Local, Enclosing, Global, Built-in.

Nesting of Scopes

It is important to remind here that nested scopes are created by defining new functions (or generators etc., see below), NOT by calling them. For example, these are not nested scopes - the function innerfn isn’t actually “inner” and the function outerfn isn’t actually “outer”. They are same-level functions, just called from one another. They do not create nested scopes.

def innerfn():
    print(x)

def outerfn():
    x = 2
    innerfn()

x = 1
outerfn()
1

We can see it because when innerfn is called from within outerfn, the number 1 is printed, and not 2. This is what happened:

  • When innerfn is called from within outerfn, the variable x is first searched for locally.
  • It is not found there, so Python completely skips the calling scope and searches for x in the scope where innerfn was defined - the global scope in this case.
  • x is found there so its value is used (1). The value 2 is not used, because Python never searches for x in the scope from which innerfn is called.
  • If x wasn’t found in the global scope (where innerfn is defined), Python would look in the built-in scope.

By the way, any variable defined anywhere in a function scope is always only searched for locally. Which means this code throws an error (UnboundLocalError):

x = 1
def fn():
    print(x)  # UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
    x = 2
fn()

At the first line of the function, we need to get the variable x. It is not yet defined locally, so it doesn’t exist in the local namespace, but it is defined in the enclosing (nonlocal) scope so we should be able to use that value, right? Wrong. Python knows in advance that somewhere down the body of the function the variable x is defined. So it treats it as a local to the function and only looks for it in the local namespace. But it is not found there at the point of the print statement, so an error is thrown.

What Creates a Scope

New scopes are created by modules, functions, methods, lambda expressions, comprehensions and generator expressions. Surprisingly, classes and their instances do not create new standard LEGB scopes, their behavior is more complicated. We will talk about that at the end of this post.

On the other hand, constructs such as if, for, while, with and try - except (with a small exception) don’t create their own scopes.

Let’s discuss each case:

Modules

Each module has its own (global) scope. If you call one module from another, the called module does NOT see variables in the calling module.

Let’s look at an example. The module a.py defines a function f which prints the variable x. But the variable x isn’t defined anywhere in the module a.py.

# Creating a new module a.py in the current working directory and populating it with code
import os

code = """
def f():
    print(x)
"""

with open("a.py", "w") as f:
    f.write(code)

Here the module a.py defines a function f that prints the variable x. But the variable x isn’t defined anywhere in the module a.py. When the function is called (doesn’t matter from where), Python first looks for x locally in f, then in the global scope of a.py and finally in the built-in scope. It is not found so a NameError is thrown. The global scope of the calling module is never searched.

import a

x = 1
a.f()  # NameError: name 'x' is not defined

But there is a way to make the function work. We can create the variable x in the module a.py from the calling module. The body of a function is, as usual, only evaluated when the function is called. And when the function a.f is called, the module a already has an attribute (or variable) x defined.

import a

a.x = 1
a.f()
1
import os
os.remove('a.py')

Functions

Functions are the most important constructs that create new scopes. This includes all possible versions of functions:

  • usual functions (keyword def)
  • class functions (usual functions defined in a class body)
  • instance methods (they are just class functions to which instances are automatically passed in as the first argument)
  • lambda expressions (which are just annonymous functions)

Comprehensions and generator expressions

They also create new scopes, just as functions do. Variables defined in them are only visible from inside of the comprehension/generator expression.

L = [i**2 for i in range(3)]
print(i)  # NameError: name 'i' is not defined
gen = (x**2 for x in range(3))
print(x)  # NameError: name 'x' is not defined
for i in gen:
    print(i)
print(x)  # NameError: name 'x' is not defined

One may think that scopes of comprehensions and generator expressions are not very interesting, since they immediatelly dissappear once the expressions are finished. But that is not true, they don’t disappear. The point of evaluation just moves back to the scope where they are created. But the inner scope still exists. We can still index into e.g. the list comprehension and access its scope. Yes, this is mostly unimportant, but it becomes important when the list elements are functions:

L = [(lambda: i) for i in range(3)]
print(L[0](), L[1](), L[2]())
2 2 2

Here, the list comprehension defines an iterative variable i that goes from 0 to 2, and for each value of i it creates an element of the list that is the function lambda: i. Meaning it is a function that returns the value of a variable i. But that variable is not the function’s parameter nor is it defined anywhere in the function.

So the functions are defined, the list is created, but the functions are not evaluated untill called (that is the usual behavior of any Python function). Then, when the list is accessed and the functions are called, their bodies are evaluated. For each function, the variable i needs to be found. Python first looks into the function locally, but doesn’t find the variable i. So Python then searches the enclosing scope, which is the list comprehension’s scope. And in that scope, the variable i exists and is now bound to the value 2. So that value is returned from each function in the list.

To make the individual functions behave in the way that each returns a different value of i (the behavior one may have wanted in the first place), we need each function to accept one variable and define its default value. Python evaluates default values at function definition time - NOT when the function is called (only the body is evaluated when function is called). So, this code returns three different values:

L = [(lambda x=i: x) for i in range(3)]
print(L[0](), L[1](), L[2]())
0 1 2

Special case: except as

Let’s look at this code:

try:
    1/0
except ZeroDivisionError as e:
    print(e)
print(e)  # NameErrorNameError: name 'e' is not defined

In this case, the except block still doesn’t define a new scope nor a new namespace, but it does define a new temporary binding for the name defined after as. This variable is then NOT visible outside the except block.

In the case of with however, e.g. as in with open('hello') as f, the name f is visible outside the with block.

with open('hello', 'w') as f:
    pass
print(f)
<_io.TextIOWrapper name='hello' mode='w' encoding='cp1252'>

Keywords global and nonlocal

So far we have seen that a variable is iteratively searched for from the local scope to the built-in scope until it is found and the corresponding value is used. By the way, it means that variables defined in higher-level scopes are read-only in lower-level scopes. I mean, we can use higher-level variables in lower-level scopes, but we cannot change them. Any attempt to change a variable defined in a higher-level scope would mean we change the variable only in the local scope:

x = 1
def fn():
    print(x)  # We can read variables defined in higher-level scopes
fn()
1
x = 1
def fn():
    x = 2
print(x)  # But we cannot change their values, as any such change would be local only and would not propagate to the higher level.
1

The keywords global and nonlocal enable us to both change higher-level variables and to manipulate the variable lookup process. For example, the keyword global says that:

  • The variable(s) following it are to be immediatelly searched for in the global namespace (skipping the local and all non-local namespaces)
  • It is possible to change these variables in the global namespace.

It is basically a direct connection to the global namespace (for that particular variable).

The keyword nonlocal does the exact same thing with the difference that it connects to the non-local scope.

We can demonstrate this behavior like this. First, we define three nested functions and define a variable x in each of them but with a different value:

x = 0
def fn1():
    x = 1
    def fn2():
        x = 2
        def fn3():
            x = 3
            print(f'inside fn3: {x = }')
        fn3()
        print(f'inside fn2: {x = }')
    fn2()
    print(f'inside fn1: {x = }')
fn1()
print(f'inside global scope: {x = }')
inside fn3: x = 3
inside fn2: x = 2
inside fn1: x = 1
inside global scope: x = 0

Then we introduce a single change: In the bottom-most scope, instead of defining x, we declare that we want to use the one defined in the global scope. We can see that indeed the value of x in the global scope is then used here in the bottom-most scope. Other scopes remained untouched.

x = 0
def fn1():
    x = 1
    def fn2():
        x = 2
        def fn3():
            global x
            print(f'inside fn3: {x = }')
        fn3()
        print(f'inside fn2: {x = }')
    fn2()
    print(f'inside fn1: {x = }')
fn1()
print(f'inside global scope: {x = }')
inside fn3: x = 0
inside fn2: x = 2
inside fn1: x = 1
inside global scope: x = 0

We can even change the global variable from the inner scope:

x = 0
print(f'at the start of global scope: {x = }')
def fn1():
    x = 1
    def fn2():
        x = 2
        def fn3():
            global x
            x = 3
            print(f'inside fn3: {x = }')
        fn3()
        print(f'inside fn2: {x = }')
    fn2()
    print(f'inside fn1: {x = }')
fn1()
print(f'at the end of global scope: {x = }')
at the start of global scope: x = 0
inside fn3: x = 3
inside fn2: x = 2
inside fn1: x = 1
at the end of global scope: x = 3

Then we see the same behavior is achieved by the keyword nonlocal, except the affected scope is the nonlocal scope.

x = 0
def fn1():
    x = 1
    def fn2():
        x = 2
        print(f'at the start of fn2: {x = }')
        def fn3():
            nonlocal x
            x = 3
            print(f'inside fn3: {x = }')
        fn3()
        print(f'at the end of fn2: {x = }')
    fn2()
    print(f'inside fn1: {x = }')
fn1()
print(f'inside global scope: {x = }')
at the start of fn2: x = 2
inside fn3: x = 3
at the end of fn2: x = 3
inside fn1: x = 1
inside global scope: x = 0

If we use nonlocal x and the variable x isn’t found in the nonlocal namespace, the search continues to higher enclosing scopes:

x = 0
def fn1():
    x = 1
    def fn2():
        def fn3():
            nonlocal x
            print(f'inside fn3: {x = }')
        fn3()
        print(f'inside fn2: {x = }')
    fn2()
    print(f'inside fn1: {x = }')
fn1()
print(f'inside global scope: {x = }')
inside fn3: x = 1
inside fn2: x = 1
inside fn1: x = 1
inside global scope: x = 0

But the search stops just before the global scope. If we declare a variable to be nonlocal, it cannot be global:

x = 0
def fn1():
    def fn2():
        def fn3():
            nonlocal x  # SyntaxError: no binding for nonlocal 'x' found
            print(f'inside fn3: {x = }')
        fn3()
        print(f'inside fn2: {x = }')
    fn2()
    print(f'inside fn1: {x = }')
fn1()
print(f'inside global scope: {x = }')

This differs from the global keyword, because when a varaible is declared global but not found in the global namespace, the next scope - built-in - is searched.

def fn1():
    def fn2():
        def fn3():
            global sum
            print(f'inside fn3: {sum = }')
        fn3()
        print(f'inside fn2: {sum = }')
    fn2()
    print(f'inside fn1: {sum = }')
fn1()
print(f'inside global scope: {sum = }')
inside fn3: sum = <built-in function sum>
inside fn2: sum = <built-in function sum>
inside fn1: sum = <built-in function sum>
inside global scope: sum = <built-in function sum>
def fn1():
    def fn2():
        def fn3():
            nonlocal sum  # SyntaxError: no binding for nonlocal 'sum' found
            print(f'inside fn3: {sum = }')
        fn3()
        print(f'inside fn2: {sum = }')
    fn2()
    print(f'inside fn1: {sum = }')
fn1()
print(f'inside global scope: {sum = }')

Functions globals and locals

There is another way to access the global namespace from within a lower-level scope: The built-in function globals. Its documentation speaks for itself.

help('globals')
Help on built-in function globals in module builtins:

globals()
    Return the dictionary containing the current scope's global variables.

    NOTE: Updates to this dictionary *will* affect name lookups in the current
    global scope and vice-versa.

Its sibling, locals, is similar but we don’t have the guarantee that we can use it to change local variables. But why would we want that? We can simply change local variables in the usual way. I mean, we don’t have to do locals()['x'] = 1, we can simply write x = 1. (We don’t have this direct option for the global variables, which is why we need the global keyword.)

help('locals')
Help on built-in function locals in module builtins:

locals()
    Return a dictionary containing the current scope's local variables.

    NOTE: Whether or not updates to this dictionary will affect name lookups in
    the local scope and vice-versa is *implementation dependent* and not
    covered by any backwards compatibility guarantees.

Classes and Scoping

This is the most difficult part of the topic. Classes differ substantially from functions in terms of scoping.

The first thing to know is that code in class bodies, unlike code in functions bodies, is evaluated immediately at the class definition time. I mean, code inside a function isn’t run until the function is called. But code inside a class body is evaluated immediately as the class is being defined

Second and probably the most important thing is that scopes of class bodies do not extend to class functions, not even to comprehensions and generator expressions used in class bodies. As a result, class bodies are NOT enclosing scopes for class functions. Also, scopes of class bodies disappear after the class is defined. Names defined in a class body are moved to the attribute dictionary of the class (__dict__) after the class is finished defining, but the scope itself disappears. As a result, the scope enclosing class functions is the scope in which the class is defined.

Third, class instances do not create new scopes either. They just create namespaces. But these namespaces do not participate in the LEGB name resolution rules. Thus, the scope enclosing instance methods is still the scope in which the class is defined.

Most of the class scoping behavior is determined by these facts.

And just for completeness: Instance attribute lookup is a completely different process than name resolutions in functions and methods. Attribute access follows completely different rules than LEGB. For instance attribute lookups, first the instance namespace is searched. Then the class namespace is searched. Then there is MRO, descriptor handling, __getattribute__ fallback etc. But that is a topic for a future post.

Let’s look at some examples of scoping in classes.

Example 1

Class body executes immediately, top to bottom. Unlike functions, which execute only after they are called.

print("before class")

class C:
    print("inside class body")
    def f(self):
        print('inside class function f')

print("after class")
C().f()
before class
inside class body
after class
inside class function f
print("before function")

def f():
    print("inside function")

print("after function")
f()
before function
after function
inside function

As a direct implication of this, we cannot reference variables defined after the class definition. Functions on the other hand can do exactly that.

def f():
    return x

x = 1
print(f())
1
class C:
    y = x  # NameError: name 'x' is not defined
    pass

x = 1

Example 2

A class body is not an enclosing LEGB scope for class functions. When a class function is called, and that function uses a variable, Python searches for that variable like this: First locally in the function, of course, and then it goes look inside the scope in which the class was defined. It completely skips the body of the class, because it is not the enclosing scope. (It also skips the scope from which the function is called, but we have seen this exact behavior for functions, this is just a reminder.)

x = 1

class C:
    x = 2
    def f():
        return x

def fn():
    x = 3
    print(C.f())
    print(C.x)
    print(x)

fn()
1
2
3

A class body is not even the enclosing scope for list comprehensions or generator expressions inside that class body. But in a weird way I don’t fully understand:

# This works
class C:
    L = [1, 2, 3]
    new = [i for i in L]
C().new
[1, 2, 3]
# This doesn't work
class C:
    x = 2
    new = [i + x for i in range(3)]  # NameError: name 'x' is not defined
C().new
# In functions however, it works:
def fn():
    x = 2
    return [i + x for i in range(3)]

fn()
[2, 3, 4]

Example 3

For instance methods, the situation is the very same as for class functions: The enclosing scope for instance methods is still the scope in which the class is defined. Instances do not create new scopes. They create namespaces (different from class namespaces), but these are not scopes. They are not searched when a name is referenced in a method. And the class body is not searched either. So again, if a name is referenced in a method, first the local scope of the method is searched, then the scope in which the class is defined.

For instance attribute lookups, first the instance namespace is searched, then the class namespace.

x = 1

class C:
    x = 2
    def f(self):
        return x

c = C()
print('c.x   is', c.x)  # instance c doesn't define x, so the class's x is used here
print('c.f() is', c.f())
c.x = 3  # now the instance c does define x, which is prioritized before the class's
print('c.x = 3')
print('c.x   is', c.x)
print('C.x   is', C.x)
print('c.f() is', c.f())
c.x   is 2
c.f() is 1
c.x = 3
c.x   is 3
C.x   is 2
c.f() is 1

Example 4

Though the scope of a class body disappears after the class is defined, until then it is still a normal scope. In particular, it does have access to all its enclosing scopes. It also creates a new namespace (that later becomes the class’s __dict__ attribute). But if we we want to reference variables defined in a class body from outside of the class body, we must access it via the class name. Btw “outside of the class body” includes class functions/instance methods, because as we have seen class body is not the enclosing scope for class functions/instance methods.

y = 1

class C:
    print(y)  # class body has access to the enclosing scope
    z = 2

    def f():
        print(C.z)

print(C.z)
C.f()
1
2
2

We can even use nonlocal in the class body to reference a variable defined in the enclosing scope. (As we have seen if we use nonlocal, the variable cannot be global, so we need to nest the class inside a function).

def fn():
    x = 2
    class C:
        nonlocal x
        print(x)

fn()
2

When nonlocal is used inside a class function/instance method, it again refers to outside of the class, not the class body.

def fn():
    x = 2
    class C:
        x = 1
        def g(self):
            nonlocal x
            print(x)

    C().g()
    print(C().x)
fn()
2
1

Example 5

A nice summarizing example. Here we can see that:

  • Code in class body is evaluated at class definition time.
  • Class functions are defined at class definition time, but their code is not run until they are envoked.
  • Class body is not the enclosing scope for class functions (instance methods)
  • We can reference class variables only via the class name.
x = 2
class C:
    print('inside class body')
    x = 1
    def f(self):
        print('inside class function')
        print("C.x is", C.x)
        print("x is", x)

print('inside global scope')
C().f()
inside class body
inside global scope
inside class function
C.x is 1
x is 2