Home > Articles

This chapter is from the book

This chapter is from the book

5.12 Scoping Rules

Each time a function executes, a local namespace is created. This namespace is an environment that contains the names and values of the function parameters as well as all variables that are assigned inside the function body. The binding of names is known in advance when a function is defined and all names assigned within the function body are bound to the local environment. All other names that are used but not assigned in the function body (the free variables) are dynamically found in the global namespace which is always the enclosing module where a function was defined.

There are two types of name-related errors that can occur during function execution. Looking up an undefined name of a free variable in the global environment results in a NameError exception. Looking up a local variable that hasn’t been assigned a value yet results in an UnboundLocalError exception. This latter error is often a result of control flow bugs. For example:

def func(x):
    if x > 0:
        y = 42
    return x + y    # y not assigned if conditional is false

func(10)    # Returns 52
func(-10)   # UnboundLocalError:   y referenced before assignment

UnboundLocalError is also sometimes caused by a careless use of in-place assignment operators. A statement such as n += 1 is handled as n = n + 1. If used before n is assigned an initial value, it will fail.

def func():
    n += 1    # Error: UnboundLocalError

It’s important to emphasize that variable names never change their scope—they are either global variables or local variables, and this is determined at function definition time. Here is an example that illustrates this:

x = 42
def func():
    print(x)    # Fails. UnboundLocalError
    x = 13

func()

In this example, it might look as though the print() function would output the value of the global variable x. However, the assignment of x that appears later marks x as a local variable. The error is a result of accessing a local variable that hasn’t yet been assigned a value.

If you remove the print() function, you get code that looks like it might be reassigning the value of a global variable. For example, consider this:

x = 42
def func():
    x = 13
func()
# x is still 42

When this code executes, x retains its value of 42, despite the appearance that it might be modifying the global variable x from inside the function func. When variables are assigned inside a function, they’re always bound as local variables; as a result, the variable x in the function body refers to an entirely new object containing the value 13, not the outer variable. To alter this behavior, use the global statement. global declares names as belonging to the global namespace, and it’s necessary when a global variable needs to be modified. Here’s an example:

x = 42
y = 37
def func():
    global x        # 'x' is in global namespace
    x = 13
    y = 0
func()
# x is now 13. y is still 37.

It should be noted that use of the global statement is usually considered poor Python style. If you’re writing code where a function needs to mutate state behind the scenes, consider using a class definition and modify state by mutating an instance or class variable instead. For example:

class Config:
    x = 42

def func():
    Config.x = 13

Python allows nested function definitions. Here’s an example:

def countdown(start):
    n = start

    def display():        # Nested function definition
        print('T-minus', n)
    while n > 0:
        display()
        n -= 1

Variables in nested functions are bound using lexical scoping. That is, names are resolved first in the local scope and then in successive enclosing scopes from the innermost scope to the outermost scope. Again, this is not a dynamic process—the binding of names is determined once at function definition time based on syntax. As with global variables, inner functions can’t reassign the value of a local variable defined in an outer function. For example, this code does not work:

def countdown(start):
    n = start
    def display():
        print('T-minus', n)
    def decrement():
        n -= 1            # Fails: UnboundLocalError
    while n > 0:
         display()
         decrement()

To fix this, you can declare n as nonlocal like this:

def countdown(start):
    n = start
    def display():
        print('T-minus', n)
    def decrement():
        nonlocal n
        n -= 1        # Modifies the outer n
    while n > 0:
         display()
         decrement()

nonlocal cannot be used to refer to a global variable—it must reference a local variable in an outer scope. Thus, if a function is assigning to a global, you should still use the global declaration as previously described.

Use of nested functions and nonlocal declarations is not a common programming style. For example, inner functions have no outside visibility, which can complicate testing and debugging. Nevertheless, nested functions are sometimes useful for breaking complex calculations into smaller parts and hiding internal implementation details.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.