Advanced · Lesson 52 of 94

Scope, closures, and nonlocal

Watch this lesson

View full playlist ↗

Read the lesson

What you'll learn

Concept

When Python sees a name, it looks in four places in order: Local (the current function), Enclosing (any function this one is defined inside), Global (the module), and Built-in (len, print, and friends). The first match wins. That is the LEGB rule, and it explains every "which x is this" question. Reading a name from an outer scope needs no special syntax; a function can use a module-level constant without ceremony.

Scope, closures, and nonlocal: Local: inside this function → Enclosing: the function around it → Global: the module → Built-in: len, print, range ...
Scope, closures, and nonlocal · Figure 1 · Open for full size

Assignment is different from reading. Assigning to a name inside a function makes that name local to the function, for the whole function, even before the line that assigns. So a function that reads count and later writes count = count + 1 fails with UnboundLocalError: the write made count local everywhere in the body, and the read happens before the local exists. The global statement says "this name means the module-level one", and nonlocal says "this name means the one in the enclosing function". Use both sparingly; a function that changes outer state is harder to reason about than one that returns a value.

You want to Inside a nested function, write Inside a top-level function, write
read an outer variable use it directly use it directly
rebind an enclosing function's variable nonlocal name (no enclosing function)
rebind a module-level variable global name global name
mutate an outer list or dict in place call .append, [k] = v same; mutation is not rebinding

A closure is a function defined inside another function that uses the outer function's variables, and keeps using them after the outer function has returned. The inner function carries its enclosing scope with it. make_multiplier(3) returns a function that multiplies by 3, and the 3 lives on inside the returned function. Closures are how decorators work, how callbacks remember context, and how you make small configurable functions without a class.

Scope, closures, and nonlocal: make_multiplier(3); factor = 3; def multiply(x): return x * factor; return multiply → triple = multiply + {factor: 3} → 15
Scope, closures, and nonlocal · Figure 2 · Open for full size

The late-binding surprise: closures capture variables, not values. If you create several functions in a loop that all use the loop variable, they all see its final value, because the variable is looked up when the function runs, after the loop has finished. The fix is to bind the current value at creation time, usually with a default argument, lambda x, n=n: x * n, or by calling a factory function that takes the value as a parameter. Once you have seen it, you recognize it instantly.

Code

LEGB in action, and the local-versus-global distinction. Reading an outer name works; assigning creates a local unless you say otherwise.

# legb.py
greeting = "hello"


def show():
    print("read global:", greeting)


def shadow():
    greeting = "hi"
    print("local shadows:", greeting)


def rebind_global():
    global greeting
    greeting = "hey"


show()
shadow()
show()
rebind_global()
show()


def broken():
    print(greeting)
    greeting = "oops"


broken()
Output
read global: hello
local shadows: hi
read global: hello
read global: hey
Traceback (most recent call last):
  File "legb.py", line 30, in <module>
    broken()
    ~~~~~~^^
  File "legb.py", line 26, in broken
    print(greeting)
          ^^^^^^^^
UnboundLocalError: cannot access local variable 'greeting' where it is not associated with a value

Closures: a factory returns functions that remember their own factor, and each returned function has independent state.

# closures.py
def make_multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply


double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5), triple(5))
print(double.__closure__[0].cell_contents, triple.__closure__[0].cell_contents)


def make_counter():
    count = 0

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


a, b = make_counter(), make_counter()
print(a(), a(), a(), "|", b())
Output
10 15
2 3
1 2 3 | 1

nonlocal versus mutation. Rebinding an enclosing variable needs the keyword; mutating an enclosing list does not, because appending is not rebinding.

# nonlocal_vs_mutate.py
def tally():
    total = 0
    items = []

    def add(x):
        nonlocal total
        total += x            # rebinding: needs nonlocal
        items.append(x)       # mutating: no keyword needed
        return total, items

    return add


add = tally()
add(5)
print(add(7))
Output
(12, [5, 7])

The late-binding surprise and both fixes. The first list of functions all multiply by 3; the default-argument and factory versions capture each value.

# late_binding.py
surprising = [lambda x: x * n for n in range(1, 4)]
print([f(10) for f in surprising])

fixed_default = [lambda x, n=n: x * n for n in range(1, 4)]
print([f(10) for f in fixed_default])


def make(n):
    return lambda x: x * n


fixed_factory = [make(n) for n in range(1, 4)]
print([f(10) for f in fixed_factory])
Output
[30, 30, 30]
[10, 20, 30]
[10, 20, 30]

Try it yourself

Create scopes.py with a module-level settings dictionary and a function that reads it, then a function that tries to rebind it and fails, then one that mutates it in place. Write a make_greeter(greeting) factory and create two greeters. Then reproduce the late-binding bug with three button handlers in a loop and fix it with a default argument, printing the results of both versions.

Common mistakes

What's next

Closures are the mechanism behind the next topic. With them you can write decorators: functions that wrap other functions to add behavior.

Course overview