Python - Python Namespaces and Scope Resolution (LEGB Rule)

Introduction

In Python, a namespace is a system that stores names and connects them to the objects they represent. Names can refer to variables, functions, classes, modules, and other Python objects. Namespaces help Python determine which object a particular name refers to when the same name is used in different parts of a program.

Scope refers to the region of a program where a particular name can be accessed directly. Python determines where to look for a name using the LEGB rule. LEGB stands for Local, Enclosing, Global, and Built-in. Understanding namespaces and the LEGB rule is important when working with functions, nested functions, modules, and variables having the same name at different levels.

What Is a Namespace?

A namespace can be considered a collection of names and the objects associated with those names.

For example:

name = "Rahul"
age = 25

Here, name refers to the string "Rahul" and age refers to the integer 25. Python maintains these associations within a namespace.

Namespaces prevent naming conflicts. Two different namespaces can contain the same name while referring to different objects.

For example:

name = "Rahul"

def display():
    name = "Priya"
    print(name)

display()
print(name)

Output:

Priya
Rahul

The name inside the function and the name outside the function belong to different scopes. Therefore, assigning "Priya" inside the function does not change the global name.

Types of Namespaces in Python

Python commonly works with several types of namespaces.

1. Local Namespace

A local namespace is created when a function is called. It contains names defined inside that function, such as local variables and parameters.

def calculate():
    number = 20
    result = number * 2
    print(result)

calculate()

Here, number and result are local names. They are available inside calculate() but normally cannot be accessed directly outside it.

def calculate():
    number = 20

calculate()

print(number)

This produces a NameError because number exists only in the local scope of calculate().

2. Enclosing Namespace

An enclosing namespace occurs when functions are nested inside other functions.

Consider:

def outer():
    message = "Hello"

    def inner():
        print(message)

    inner()

outer()

The variable message belongs to the outer() function. The nested inner() function does not have a local variable named message, so Python looks in the enclosing scope and finds it there.

This is called an enclosing scope.

3. Global Namespace

The global namespace belongs to the module or Python file currently being executed.

For example:

language = "Python"

def display():
    print(language)

display()

Here, language is defined outside the function, so it belongs to the global namespace. The function can access it because Python searches the global scope when it cannot find the name locally.

4. Built-in Namespace

The built-in namespace contains names that Python provides automatically.

Examples include:

print()
len()
type()
sum()
max()
min()

For example:

numbers = [10, 20, 30]

print(len(numbers))

Python finds len in the built-in namespace.

The built-in namespace is the final level Python checks when resolving a name using the LEGB rule.

Understanding the LEGB Rule

LEGB stands for:

L – Local
E – Enclosing
G – Global
B – Built-in

When Python encounters a name, it searches these scopes in this order.

Local
  ↓
Enclosing
  ↓
Global
  ↓
Built-in

Python stops searching when it finds the name.

1. Local Scope

Python first checks the current function's local scope.

name = "Global"

def display():
    name = "Local"
    print(name)

display()

Output:

Local

There are two variables called name, but the local variable takes precedence because Python checks the local scope first.

2. Enclosing Scope

If Python cannot find a name locally, it checks the enclosing function when nested functions are involved.

def outer():
    message = "From outer"

    def inner():
        print(message)

    inner()

outer()

Output:

From outer

There is no local message inside inner(), so Python searches the enclosing outer() scope.

3. Global Scope

If a name is not found in either the local or enclosing scope, Python searches the global scope.

message = "Global message"

def display():
    print(message)

display()

Output:

Global message

The function does not contain a local message, so Python finds the global variable.

4. Built-in Scope

If Python cannot find the name in the local, enclosing, or global scopes, it checks the built-in namespace.

numbers = [10, 20, 30]

result = len(numbers)

print(result)

There is no variable named len in the local or global scope. Python therefore finds the built-in len() function.

If a name cannot be found anywhere in the LEGB chain, Python raises a NameError.

def display():
    print(value)

display()

If value has not been defined in any applicable scope, Python reports:

NameError: name 'value' is not defined

Local and Global Variables

One of the most important aspects of scope is understanding the difference between reading and modifying global variables.

Consider:

count = 10

def display():
    print(count)

display()

This works because the function is only reading the global variable.

However, the following code behaves differently:

count = 10

def increase():
    count = count + 1
    print(count)

increase()

Python treats count as a local variable because there is an assignment to it inside the function. The function therefore attempts to use a local count before it has been assigned a value, resulting in an UnboundLocalError.

The global Keyword

The global keyword tells Python that a variable inside a function should refer to a variable in the global namespace.

count = 10

def increase():
    global count
    count = count + 1

increase()

print(count)

Output:

11

The statement:

global count

means that assignments to count inside the function should modify the global variable.

Although global can be useful, excessive use of global variables can make programs harder to understand and maintain. Functions that depend heavily on global state can also become more difficult to test.

The nonlocal Keyword

The nonlocal keyword is used with nested functions. It allows an inner function to modify a variable belonging to its enclosing function.

Example:

def counter():
    count = 0

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

    return increase

my_counter = counter()

print(my_counter())
print(my_counter())
print(my_counter())

Output:

1
2
3

Here, count belongs to counter(), not to increase() and not to the global namespace.

The nonlocal keyword tells increase() to use the variable from its enclosing scope.

Without nonlocal:

def counter():
    count = 0

    def increase():
        count += 1
        return count

    return increase

Python treats count as a local variable inside increase(), which causes an UnboundLocalError.

Variable Shadowing

Variable shadowing occurs when a name in a narrower scope has the same name as a name in an outer scope.

name = "Rahul"

def display():
    name = "Priya"
    print(name)

display()
print(name)

Output:

Priya
Rahul

The local name shadows the global name while the function is executing.

Shadowing can also occur with built-in names.

For example:

len = 100

numbers = [1, 2, 3]

print(len(numbers))

This causes an error because the global variable len has replaced the normal reference to Python's built-in len() function.

For this reason, it is better to avoid using names such as list, str, sum, type, and len as your own variable names.

Scope in Loops and Conditional Statements

Python has an important distinction between function scope and block scope.

In languages that use block scope, variables created inside an if or for block may not be available outside that block. Python does not generally create a new local scope for ordinary if, for, or while blocks.

For example:

if True:
    message = "Hello"

print(message)

Output:

Hello

Similarly:

for number in range(3):
    value = number

print(value)

Output:

2

However, functions do create their own local scope.

def display():
    message = "Hello"

display()

print(message)

This results in a NameError because message belongs to the function's local scope.

Scope in Nested Functions

Nested functions provide a clear demonstration of the LEGB rule.

message = "Global"

def outer():
    message = "Enclosing"

    def inner():
        message = "Local"
        print(message)

    inner()

outer()

Output:

Local

Python finds message immediately in the local scope of inner().

If the local variable is removed:

message = "Global"

def outer():
    message = "Enclosing"

    def inner():
        print(message)

    inner()

outer()

Output:

Enclosing

Now Python searches the enclosing scope and finds message in outer().

If that variable is also removed:

message = "Global"

def outer():

    def inner():
        print(message)

    inner()

outer()

Output:

Global

Python eventually searches the global namespace.

Why the LEGB Rule Is Important

The LEGB rule helps developers understand how Python resolves variable and function names. It is particularly important when programs contain nested functions, modules, classes, callbacks, or variables with identical names.

Understanding LEGB helps with:

  1. Avoiding unexpected variable conflicts.

  2. Understanding NameError and UnboundLocalError.

  3. Working correctly with global and nested variables.

  4. Designing functions with appropriate scope.

  5. Understanding closures and nested functions.

  6. Debugging programs where a variable does not contain the expected value.

  7. Preventing accidental shadowing of built-in functions.

  8. Writing cleaner and more maintainable Python programs.

Example Combining All Four LEGB Levels

The following example demonstrates the complete concept:

message = "Global"

def outer():
    message = "Enclosing"

    def inner():
        message = "Local"
        print(message)

    inner()

outer()
print(len("Python"))

Inside inner(), Python first searches the local scope and finds:

message = "Local"

Therefore, "Local" is printed.

When Python encounters len(), it searches the applicable namespaces. Since there is no locally defined or global replacement for len, Python eventually finds the built-in len() function.

Summary

Namespaces provide the mechanism through which Python associates names with objects, while scopes determine where those names can be accessed. Python follows the LEGB rule when resolving names: Local, Enclosing, Global, and Built-in.

The local scope has the highest priority, followed by the enclosing scope, global scope, and finally the built-in scope. The global keyword allows a function to work with a global variable, while the nonlocal keyword allows a nested function to modify a variable in its enclosing function.

A strong understanding of namespaces and the LEGB rule is essential for writing reliable Python programs, particularly when working with functions, nested functions, closures, modules, and variables that share the same names.