In Python, the concept of scope defines the region of a program where a particular name (like a variable or function) is accessible and can be referenced. The rules that determine this accessibility are collectively known as the LEGB rule, which is a mnemonic for the order in which Python searches for a name: Local, Enclosing, Global, Built-in. Understanding this hierarchy is fundamental to writing predictable, bug-free code.

The Four LEGB Scopes

The LEGB rule describes a chain of scopes that Python traverses, in order, to resolve a name.

Local Scope

The local scope is the innermost scope, created whenever a function is called. It contains the names defined within that function, including its parameters. These names are only accessible from the point of their assignment until the end of the function block. Once the function returns, its local scope is destroyed.

def calculate_tax(price):
    tax_rate = 0.08  # Local variable
    return price * tax_rate

result = calculate_tax(100)
print(result)        # Output: 8.0
# print(tax_rate)    # This would raise a NameError: name 'tax_rate' is not defined

Enclosing Scope

The enclosing scope (or nonlocal scope) is relevant in the context of nested functions. It refers to the scope of any enclosing function that contains the current function. This allows inner functions to access names from their outer functions, but not modify them directly without the nonlocal keyword.

def outer_function():
    message = "I'm from the enclosing scope!"  # Enclosing (nonlocal) variable

    def inner_function():
        print(message)  # Inner function can access the enclosing scope's variable

    inner_function()

outer_function()  # Output: I'm from the enclosing scope!

Global Scope

The global scope is the scope of the module (a .py file). Names defined at the top-level of a module, outside of any function or class, are global. They are accessible from any function or class within that module. To modify a global variable from within a function, the global keyword must be used.

global_message = "I'm a global variable!"  # Global variable

def print_global():
    print(global_message)  # Function can access the global variable

def modify_global_badly():
    global_message = "This creates a new local variable!"  # This does NOT modify the global

def modify_global_correctly():
    global global_message  # Explicitly declare we mean the global one
    global_message = "Now I've modified the global variable!"

print_global()              # Output: I'm a global variable!
modify_global_badly()
print_global()              # Output: I'm a global variable! (unchanged)
modify_global_correctly()
print_global()              # Output: Now I've modified the global variable!

Built-in Scope

The built-in scope is the widest scope. It contains all the built-in names that are available by default in Python, such as print, len, str, list, and Exception. This scope is searched last if a name is not found in any of the local, enclosing, or global scopes.

def example():
    # Python finds the built-in `len` function
    return len([1, 2, 3])

print(example())  # Output: 3

The global and nonlocal Keywords

These keywords are essential for explicitly controlling variable binding in higher scopes from within a nested scope.

The global Keyword

The global keyword is used to declare that a variable inside a function belongs to the global scope. It is required both for modifying an existing global variable and for creating a new global variable from within a function.

counter = 0  # Global

def increment():
    global counter
    counter += 1  # Without 'global', this would be an UnboundLocalError

increment()
print(counter)  # Output: 1

The nonlocal Keyword

The nonlocal keyword is used in nested functions to indicate that a variable belongs to the nearest enclosing scope that is not global. It allows an inner function to rebind a variable from an outer (but non-global) scope.

def outer():
    state = "Original"  # Enclosing scope variable

    def inner():
        nonlocal state  # Declare intent to use the enclosing scope's variable
        state = "Modified"  # Rebind it

    inner()
    print(state)  # Output: Modified

outer()

Common Pitfalls and Best Practices

A frequent pitfall is shadowing a built-in function by accident, which can lead to confusing errors.

# PITFALL: Shadowing a built-in
list = [1, 2, 3]  # This redefines 'list' in the global scope!

def get_length():
    # Now list refers to the global variable, not the built-in type
    # return len(list)  # This would work, but it's dangerous.
    pass

# This will now fail because 'list' is no longer the built-in function.
# list_of_tuples = list([(1, 2), (3, 4)])  # TypeError: 'list' object is not callable

Best Practice: Avoid using names of built-in functions (list, str, dict, id, max, etc.) for your variables to prevent this shadowing.

Another critical best practice is to minimize the use of global. Relying heavily on global variables makes code harder to reason about, debug, and test because functions are no longer self-contained units with explicit inputs and outputs. Instead, prefer passing variables as arguments and returning results. Use global and nonlocal sparingly and only when there is a clear, justified need.