Skip to main content

Python Scope & LEGB Rule: Variable Visibility Guide

Scope determines where in your code a variable can be accessed. Python uses the LEGB rule—Local, Enclosing, Global, Built-in—to search for variable names in order of proximity to your code. Understanding scope prevents common bugs where variables seem to "disappear," have unexpected values, or create naming conflicts across functions. This guide demystifies Python's variable lookup mechanism and teaches you to use global and nonlocal keywords effectively.


The LEGB Rule: Python's Variable Lookup Order

When you reference a variable name, Python does not search randomly; it follows a deterministic order called the LEGB rule. Python checks these four scopes in this exact sequence until it finds the name:

  1. L — Local Scope: The current function's local namespace. Searched first. Contains names defined inside the current function (including parameters).
  2. E — Enclosing Scope: The scope of any enclosing (outer) function. Searched only if the name is not found locally. Exists only for nested functions.
  3. G — Global Scope: The module-level namespace at the top of your script. Searched if not found in local or enclosing scopes. Persists for the entire program execution.
  4. B — Built-in Scope: The namespace of Python's built-in names. Always searched last. Contains functions like print(), len(), str(), exceptions like ValueError, and constants like True, False, None.

If Python exhausts all four scopes and does not find the name, it raises a NameError: name 'x' is not defined. This error message tells you the variable was never created in any accessible scope.

The Lookup Algorithm in Action

x = "global"  # Global scope

def outer():
y = "enclosing" # Enclosing scope for inner()

def inner():
z = "local" # Local scope
print(x) # Search order: Local (no) -> Enclosing (no) -> Global (yes!) -> prints "global"
print(y) # Search order: Local (no) -> Enclosing (yes!) -> prints "enclosing"
print(z) # Search order: Local (yes!) -> prints "local"

inner()

outer()

Local Scope: Variables Inside Functions

Any variable defined inside a function belongs to that function's local scope. Local variables are created when the function is called and destroyed when the function returns—this is called the variable's lifetime. A local variable cannot be accessed outside its function; attempting to do so raises NameError.

def greet(name):
greeting = f"Hello, {name}!" # 'greeting' is local to greet()
print(greeting)

greet("Alice") # Output: Hello, Alice!

# This line would raise NameError: name 'greeting' is not defined
# print(greeting)

Parameters are also local variables. In the example above, name is local to greet() and cannot be accessed outside the function. This is why local scope is critical for function encapsulation—functions can work with their own data without interfering with other parts of the program.


Global Scope: Module-Level Variables

A variable defined at the top level of your script (outside any function or class) is in global scope. Global variables can be read from anywhere in your script, including inside functions. However, by default, functions cannot modify global variables—Python assumes assignment creates a new local variable.

Reading Global Variables (Works by Default)

counter = 0  # Global variable

def increment_display():
# This function can READ the global counter
print(f"Counter is: {counter}")

increment_display() # Output: Counter is: 0

Writing to Global Variables (Requires global Keyword)

counter = 0  # Global variable

def increment_global():
global counter # Declare intent to modify the global 'counter'
counter += 1
print(f"Counter is now: {counter}")

increment_global() # Output: Counter is now: 1
increment_global() # Output: Counter is now: 2

print(counter) # Output: 2 (global counter was modified)

Without the global keyword, the assignment creates a new local variable:

counter = 0

def bad_increment():
counter += 1 # Python sees assignment and creates a new local 'counter'
# But 'counter += 1' tries to READ 'counter' first
# UnboundLocalError: local variable 'counter' referenced before assignment

bad_increment()

This error is deceptive—it tells you the variable is unbound locally, not that it does not exist globally.


Enclosing Scope: Nested Functions and Closures

Enclosing scope only exists for nested functions (functions defined inside other functions). An inner function can read variables from its outer function's scope, creating a closure—the inner function "remembers" the enclosing scope even after the outer function returns.

def make_counter():
count = 0 # Enclosing scope for increment()

def increment():
nonlocal count # Declare intent to modify enclosing 'count'
count += 1
return count

return increment # Return the inner function

counter_func = make_counter()
print(counter_func()) # Output: 1
print(counter_func()) # Output: 2
print(counter_func()) # Output: 3

Each call to counter_func() increments the same count variable from make_counter()'s scope. This is powerful for creating stateful functions without global variables. The enclosing scope is "captured" by the returned function.


Built-in Scope: Python's Predefined Names

The built-in scope contains all names that Python provides by default: print(), len(), dict(), ValueError, True, False, None, enumerate(), and hundreds more. You access these directly without importing because Python loads them at startup.

# These are in built-in scope; no import needed
print(len([1, 2, 3])) # Output: 3
result = max([5, 2, 8]) # Output: 8

Built-in scope is searched last, so if you define a function or variable with the same name as a built-in, your local definition shadows (hides) the built-in in that scope:

def print_number(x):
print = len # WRONG: shadows the built-in print()
print(x) # Calls len(), not print()

# Avoid shadowing built-ins!

The global Keyword: Modifying Global Variables

Use the global keyword to declare that an assignment or modification inside a function refers to a global variable, not a new local variable.

total_sales = 0

def record_sale(amount):
global total_sales
total_sales += amount

record_sale(100)
record_sale(50)
print(total_sales) # Output: 150

The global keyword must appear before the variable is assigned. It affects the entire function, not just the specific line.

Common Mistakes with global

Mistake 1: Forgetting the global keyword

x = 10

def modify_x():
x = 20 # Creates a NEW local 'x', does not modify global
print(x) # Output: 20

modify_x()
print(x) # Output: 10 (global unchanged)

Mistake 2: Declaring global after first use

y = 10

def bad_use():
print(y) # Tries to read 'y' before declaring it global
global y # Too late!
# UnboundLocalError

The nonlocal Keyword: Modifying Enclosing Scope Variables

Use the nonlocal keyword inside a nested function to modify a variable from the enclosing function's scope. This is essential for closures and stateful inner functions.

def outer():
value = 10

def inner():
nonlocal value # Refer to 'value' from enclosing scope
value += 5
print(f"Inner: {value}")

inner()
print(f"Outer: {value}")

outer()
# Output:
# Inner: 15
# Outer: 15

Without nonlocal, the assignment would create a new local variable in inner():

def outer():
value = 10

def inner():
value += 5 # Creates a NEW local 'value'
# UnboundLocalError: local variable 'value' referenced before assignment

inner()

Scope in Practice: Real-World Patterns

Pattern 1: Factory Functions with Closures

def make_multiplier(factor):
def multiplier(x):
return x * factor # Reads 'factor' from enclosing scope
return multiplier

times_three = make_multiplier(3)
times_five = make_multiplier(5)

print(times_three(10)) # Output: 30
print(times_five(10)) # Output: 50

Each returned function captures its own factor value in its enclosing scope.

Pattern 2: Decorator Functions

Decorators rely heavily on enclosing scope:

def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}") # Accesses 'func' from enclosing scope
return func(*args, **kwargs)
return wrapper

@log_calls
def add(a, b):
return a + b

add(2, 3) # Output: Calling add, returns 5

Key Takeaways

  • LEGB is the lookup order: Python searches Local, then Enclosing, then Global, then Built-in when resolving variable names.
  • Local variables are default: Any assignment inside a function creates a local variable unless you declare global or nonlocal.
  • Read is free, write is explicit: Functions can read from outer scopes, but modifying outer-scope variables requires global (for global) or nonlocal (for enclosing).
  • Global should be rare: Overuse of global variables makes code hard to reason about. Prefer passing values as function arguments.
  • Closures are powerful: Enclosing scope enables closures, which create stateful functions without global state.
  • Avoid shadowing built-ins: Do not name your variables print, len, list, etc., as this hides Python's built-ins in that scope.

Frequently Asked Questions

Why does Python create a local variable on assignment instead of modifying the global one?

This design prevents bugs. If Python modified globals by default, a typo in a variable name could silently modify global state, creating hard-to-find bugs. Requiring explicit global declaration makes intent clear and prevents accidents.

Can I use global to create a new global variable inside a function?

Yes. If you declare a variable global inside a function and assign to it, Python creates that variable in global scope if it does not exist:

def make_global():
global new_var
new_var = 42

make_global()
print(new_var) # Output: 42

However, doing this is generally poor practice; define globals at module level for clarity.

What is the difference between global and nonlocal?

global refers to module-level scope (the top of your script). nonlocal refers to the immediate enclosing function's scope. Use global when the outer function is the module itself; use nonlocal when there is a function between your current function and the global level.

Can I modify a mutable global object (like a list) without the global keyword?

Yes. The global keyword is needed for reassignment, not for mutation:

my_list = [1, 2, 3]

def add_to_list(x):
my_list.append(x) # Mutates the global list; no 'global' keyword needed

add_to_list(4)
print(my_list) # Output: [1, 2, 3, 4]

This works because you are not reassigning my_list, just calling methods on it.

Do I need global inside a class method to modify an instance variable?

No. Instance variables are accessed via self, not resolved by the LEGB rule. Use self.variable = value to modify instance state without the global keyword.


Further Reading