Execute Program

Python for Programmers: Nonlocal and Global Scope

Welcome to the Nonlocal and Global Scope lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • Python gives us fine-grained control over global and "nonlocal" variable access, which is unusual among programming languages. We'll briefly look at Python's sensible scoping rules, which match most languages. Then we'll see global and nonlocal, which let us control variable scope.

  • Local variables "shadow" global variables. If a local and global variable both have the name x, we can only access the local variable x. The global variable x is shadowed, and inaccessible, since we can only have one variable named x in a given scope.

  • >
    x = 5

    def increment(initial_value):
    x = initial_value
    x = x + 1
    return x
  • In the example above, x = initial_value creates a new local variable inside the function. This local x shadows the global x. Note that it doesn't modify the global x!

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    incremented_x = increment(10)
    incremented_x
    Result:
    11Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    incremented_x = increment(10)
    x
    Result:
    5Pass Icon
  • Sometimes we want more fine-grained control over how scoping works. For example, what if we really do want the above function to modify the global variable x?

  • In Python, we can use the global statement to explicitly say "x in this function refers to the global x, not a local variable x". Here's the same code example again. The only difference is that we've added global. Now, calling increment modifies the global x. That global change is visible even after increment returns.

  • >
    x = 5

    def increment(initial_value):
    global x
    x = initial_value
    x = x + 1
    return x
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    incremented_x = increment(10)
    incremented_x
    Result:
    11Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    incremented_x = increment(10)
    x
    Result:
    11Pass Icon
  • There's more to scope than just locals and globals. We might have multiple nested scopes, like when we define a function inside of another function.

  • The next example tries to create a stateful counter. When we do increment = create_counter(0), we get a new function stored in increment. When we later call that increment function, it increments its value, then returns that value to us.

  • >
    def create_counter(initial_value):
    x = initial_value

    def increment():
    x = x + 1
    return x

    return increment
  • Note the two different x variables: one in the outer function, and one in the inner function. That code would work in many languages, but it doesn't work in Python.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    increment = create_counter(0)
    increment()
    Result:
  • The problem is in the line x = x + 1. Because we have x = ... in the function, references to x are treated as a reference to a local variable x. The line x = x + 1 tries to read from that local variable. But this is the first line in the function, so x has no value to read.

  • When we actually want to modify a variable in an outer scope, we have to explicitly indicate that with the nonlocal statement. The variable is "non-local" because it's not in the current function's local scope, but it's also not in the global scope. We can think of it as being between local and global.

  • Here's the same create_counter function, but with the nonlocal statement added.

  • >
    def create_counter(initial_value):
    x = initial_value

    def increment():
    nonlocal x
    x = x + 1
    return x

    return increment

    increment = create_counter(0)
  • Now our counter works as intended. When we call it, it increments the value and returns the result.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    increment()
    Result:
    1Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    increment()
    Result:
    2Pass Icon
  • What if we want to create a new local variable, but initialize it from a value in the outer scope? We already saw that we can't read x from the outer scope, then assign to a new x in the local scope. However, there's an easy workaround: just use a different name in the inner scope!

  • >
    x = 1

    def f():
    inner_x = x + 1
    return inner_x

    new_x = f()
    [new_x, x]
    Result:
    [2, 1]Pass Icon
  • Why does Python have both global and nonlocal? Don't they mean the same thing?

  • They both refer to variables outside of the current local scope. But nonlocal won't let us access global variables. It only works for variables defined in another non-global scope, like the nested functions that we saw above. Trying to access a global with nonlocal causes an error.

  • >
    x = 5

    def increment(initial_value):
    nonlocal x
    x = initial_value
    x = x + 1
    return x

    incremented_x = increment(10)
    Result:
    SyntaxError: no binding for nonlocal 'x' found (<string>, line 4)Pass Icon
  • Inversely, global only refers to values at the top level of the current module. Other names that might be visible from outer scopes are not considered.

  • In the next example, the z = 3 line modifies the global variable z.

  • >
    z = 1

    def level_1():
    z = 2

    def level_2():
    global z
    z = 3

    level_2()

    level_1()
    z
    Result:
    3Pass Icon
  • Here's a code problem:

    Write a function that returns a boolean indicating whether provided_pin matches correct_pin. If the PIN doesn't match, increment the global variable failed_attempts by 1.

    failed_attempts = 0
    def check_pin(correct_pin, provided_pin):
    global failed_attempts

    if correct_pin == provided_pin:
    return True
    else:
    failed_attempts += 1
    return False
    check_1 = check_pin("1234", "5678")
    check_2 = check_pin("9000", "9000")
    check_3 = check_pin("867", "5309")

    [check_1, check_2, check_3, failed_attempts]
    Goal:
    [False, True, False, 2]
    Yours:
    [False, True, False, 2]Pass Icon