Python Namespace and Variable Scope Resolution (LEGB)

When a function uses a name, Python searches the namespaces visible to that code block to find its binding. Scope is the region where a name can be resolved, and LEGB names the order Python searches those namespaces.

I’ll trace name lookup through LEGB, then show why reading a local name before assigning to it raises an error.

TL;DR: Python variable scope follows LEGB

Python looks for a name in the current Local scope, then enclosing function scopes, the module Global scope, and the Built-in namespace. A name assigned inside a function is local to that function unless the function declares it global or nonlocal.

  • Namespaces map names to objects. Scope determines where a name can be used.
  • Assignments in if and loop blocks do not create a separate function scope.
  • Use global for a module-level binding and nonlocal for a binding in an enclosing function.

What is Python scope and namespace?

A namespace is a mapping from names to objects, while scope is the region where a name can be resolved. When Python evaluates a name, it searches the namespaces visible to the current code block.

Functions introduce local scopes, and nested functions can see names from enclosing functions. A module has its own global namespace, and Python supplies a built-in namespace for names such as len. Indented if and for blocks do not create a new function scope.

Trace a name through Python’s LEGB scopes

This example gives x a different binding at each nested level. The call to len also resolves through the built-in namespace.

Step 1: Compare local, enclosing, and global names

Each print reads the nearest binding visible from that function. The same identifier can refer to different objects at different levels.

x = "global"

def outer():
    x = "enclosing"

    def inner():
        x = "local"
        print("local:", x)
        print("builtin:", len("abc"))

    inner()
    print("enclosing:", x)

outer()
print("global:", x)

The output shows each x at its own level, while len resolves to the built-in function.

local: local
builtin: 3
enclosing: enclosing
global: global

Step 2: Rebind an enclosing name with nonlocal

An assignment inside inner normally creates a local name. The nonlocal declaration instead points the assignment at the nearest enclosing function binding.

def make_counter():
    count = 0

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

    return increment

next_count = make_counter()
print("counter:", next_count(), next_count())

The returned function retains access to count, and the two calls print increasing values.

counter: 1 2

When scope lookup behaves differently than expected

The lookup order is only part of the rule. Python classifies a function name as local when that function binds it anywhere in its body, so reading it before the assignment raises UnboundLocalError rather than falling through to the global binding.

This is why a variable assigned in an if statement or loop can still be available later in the same function. Those blocks do not create their own function scope, although the name may not have been assigned if the branch or loop body never ran.

Use global when a function must rebind a module-level name. Use nonlocal when it must rebind a name in an enclosing function. Without either declaration, assignment binds a new local name.

Check the local-binding error

I ran this function with the global x from the earlier example. Because the assignment makes x local throughout the function, the first print cannot read the global value.

def read_then_assign():
    print(x)
    x = "local"

read_then_assign()

The run raised UnboundLocalError at print(x). A name that is not bound in any visible scope raises NameError instead. UnboundLocalError is a subclass of NameError.