Python Scope

Tutorial 24 of 65 · pythondeck.com Python course

Python looks up names using the LEGB rule: Local, Enclosing, Global, Built-in. Use global to rebind a module-level name inside a function, and nonlocal to rebind a name from an enclosing function scope.

Scope rules decide which variable name refers to which object. Python resolves names at runtime using LEGB: Local, Enclosing (nested functions), Global (module), Built-in. Misunderstanding scope causes classic bugs: accidental shadowing, UnboundLocalError, and closures that never see updated values.

Functions create a new local namespace on each call. Assigning to a name inside a function makes it local unless you declare global or nonlocal. Reading a name looks outward through enclosing scopes until a binding is found.

LEGB lookup order for name resolution.

Local namespace: parameters and assignments inside the function.

global to rebind module-level names from inside a function.

nonlocal to rebind names in an enclosing (non-global) function.

Closures: inner functions that capture variables from outer scopes.

The locals() and globals() dictionaries (debugging, metaprogramming).

List comprehensions and generator expressions have their own local scope in Python 3 (the loop variable does not leak). Class bodies are special: assignments create class attributes, not instance attributes, until self.attr is used in methods.

Default argument values are evaluated once at function definition time. Mutable defaults like def f(items=[]) are a notorious scope-related footgun; use None and create a new list inside the function.

Closures in loops often capture the variable itself, not its value at each iteration. Use default arguments (lambda i=i: i) or functools.partial to bind the current value.

Using global everywhere instead of passing parameters or returning values.

Forgetting nonlocal when an inner function must update a counter in the enclosing function.

Assuming the loop variable in a comprehension is visible after the comprehension (it is not in Python 3).

Mutable default arguments shared across all calls.

Shadowing built-ins like list or id in a local or global scope.

Keep functions pure where possible: explicit inputs and outputs beat hidden globals.

Use nonlocal sparingly for small closures (factories, decorators); prefer classes for stateful objects.

Never use mutable literals as default parameter values.

Run python -m dis on confusing functions to see STORE_FAST vs STORE_GLOBAL.

Re-read the examples below with these ideas in mind; change variable names and inputs to match your own project.

The program below demonstrates local vs global. Read the comments on each line, run the code, then change names or values to see how the output shifts.

# Example: Local vs global
# Run in the REPL or save as a .py file and execute with python.
x = 1
def f():
    x = 2
    print("inside", x)
f()
print("outside", x)

This sample walks through global in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.

# Example: global
# Run in the REPL or save as a .py file and execute with python.
counter = 0
def bump():
    global counter
    counter += 1
for _ in range(3):
    bump()
print(counter)

Here is a hands-on illustration of nonlocal. Follow the inline comments first; only then execute the snippet and compare the result with what you expected.

# Example: nonlocal
# Run in the REPL or save as a .py file and execute with python.
def make_counter():
    n = 0
    def inc():
        nonlocal n
        n += 1
        return n
    return inc

c = make_counter()
print(c(), c(), c())

The program below demonstrates local global. Read the comments on each line, run the code, then change names or values to see how the output shifts.

# Assignment inside a function creates a local name unless declared global
count = 0  # module-level counter

def bump():  # function scope demo
    global count  # opt-in to mutate module variable
    count += 1  # now legal

bump(); bump()  # call twice
print(count)  # 2
def outer():  # nested scopes
    msg = "outer"  # enclosing variable
    def inner():  # nested function
        nonlocal msg  # bind to enclosing name
        msg = "inner"  # mutate closure cell
    inner(); return msg  # return updated msg
print(outer())  # inner

This sample walks through legb lookup in a small, runnable script. Paste it into the REPL or save it as a .py file before you continue to the next block.

# Name resolution order: Local, Enclosing, Global, Builtins
x = "global"  # module global

def demo():  # function with shadowing
    x = "local"  # shadows global inside demo
    print(x)  # local
    def nested():  # nested scope
        print(x)  # reads enclosing local
    nested()  # call nested

demo()  # prints local then local
print(x)  # still global string
print(len([1, 2, 3]))  # len resolved from builtins
print(globals().get("x"))  # access global namespace dict

« Python Polymorphism All tutorials Python Modules »