Technology  /  Python

🐍 Python 78 guides · updated 2026

From first variable to OOP, generators, and real projects — the language that runs everything from data pipelines to AI agents, taught the practical way.

Python Variable Scope: Local, Enclosing, Global, and Built-in (LEGB Rule)

Every name in a Python program exists in a scope — a region of code where that name is accessible. When Python encounters a name like total or calculate, it searches through a chain of scopes in a specific order to find it. Understanding this order prevents confusing bugs and helps you write functions that are predictable and self-contained.

The LEGB Rule

Python searches for names in this order:

  1. Local — inside the current function
  2. Enclosing — inside any outer functions (relevant for nested functions)
  3. Global — at the module level
  4. Built-in — Python’s built-in names (len, print, range, etc.)

The first match wins. If no match is found at any level, Python raises a NameError.

Local Scope

Any name assigned inside a function is local to that function. It does not exist before the function is called and is destroyed when the function returns.

def calculate_area(radius):
pi = 3.14159 # local variable
area = pi * radius ** 2 # also local
return area
print(calculate_area(5)) # 78.53975
# print(pi) # NameError — pi does not exist outside the function

Local scope provides isolation: two functions can both have a variable named result without conflicting with each other.

Global Scope

Names assigned at the module level (outside any function or class) are in the global scope. Any function in that module can read them.

MAX_RETRIES = 3 # global constant
def attempt_connection(host):
for i in range(MAX_RETRIES): # reads global without issue
print(f"Attempt {i + 1} connecting to {host}")
# ...

Reading a global variable is straightforward. Modifying it inside a function requires declaring global first. Without it, Python treats any assignment inside the function as creating a new local variable:

request_count = 0 # global
def handle_request():
global request_count # declares intent to modify the global
request_count += 1
handle_request()
handle_request()
print(request_count) # 2

Without global request_count, the line request_count += 1 raises UnboundLocalError because Python sees the assignment and marks request_count as a local variable for the entire function — but it has no local value to read from before the += 1.

Why Global State Is a Design Smell

Using mutable global variables creates hidden dependencies between functions. Any function anywhere in the module can change the global, making it hard to reason about the state at any given point.

# Fragile — side effects through global state
counter = 0
def increment():
global counter
counter += 1
def reset():
global counter
counter = 0
# Testing increment() requires knowing the value of counter first

A better approach passes state explicitly:

# Better — functions work on the data they receive
def increment(counter):
return counter + 1
def reset():
return 0
count = 0
count = increment(count)
count = increment(count)
print(count) # 2

Global constants (configuration, mathematical constants) are fine. Mutable global variables that functions modify are a sign the code should be restructured — perhaps into a class.

Enclosing Scope and Closures

When functions are nested, the inner function can access names from the outer function’s scope. This is the enclosing scope, and the combination of the inner function plus the enclosed variables is called a closure.

def make_multiplier(factor):
def multiply(number):
return number * factor # factor comes from enclosing scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
print(double(10)) # 20

multiply references factor from make_multiplier’s scope. Each call to make_multiplier creates a new enclosing scope with its own factor, so double and triple maintain separate state.

Closures are how Python implements many functional programming patterns, decorators, and factory functions.

nonlocal: Modifying an Enclosing Variable

Just as global allows modification of a module-level variable, nonlocal allows an inner function to modify a variable in its enclosing function’s scope.

def make_counter():
count = 0
def increment():
nonlocal count # modify the enclosing variable
count += 1
return count
def reset():
nonlocal count
count = 0
return increment, reset
inc, rst = make_counter()
print(inc()) # 1
print(inc()) # 2
print(inc()) # 3
rst()
print(inc()) # 1

Without nonlocal, each count += 1 inside increment would create a local count variable and raise UnboundLocalError.

Built-in Scope

The built-in scope contains Python’s built-in names: print, len, range, list, dict, type, isinstance, and hundreds more. It is always the last place Python looks.

You can shadow built-ins by creating a local or global variable with the same name — which is usually a mistake:

# Accidental shadowing — now "list" refers to your variable, not the built-in
list = [1, 2, 3]
print(list) # [1, 2, 3]
list([4, 5, 6]) # TypeError — list is now a variable, not a function
# Fix: delete or rename
del list
print(list([4, 5, 6])) # [4, 5, 6] — built-in is accessible again

Avoid naming local or global variables the same as built-in names.

Practical Example: Scope in Context

DISCOUNT_RATE = 0.1 # global constant
def calculate_order(items):
subtotal = sum(price for _, price in items) # local
discount = subtotal * DISCOUNT_RATE # reads global
total = subtotal - discount # local
return total
order = [("Widget", 29.99), ("Gadget", 49.99), ("Doodad", 9.99)]
print(f"Total: £{calculate_order(order):.2f}")
# Total: £80.97

DISCOUNT_RATE is a module-level constant — reading it from a function is appropriate. subtotal, discount, and total are local — they exist only for the duration of calculate_order.

Summary

ScopeWhereRead?Modify without keyword?
LocalInside current functionYesYes
EnclosingOuter function (closures)Yesnonlocal needed
GlobalModule levelYesglobal needed
Built-inPython internalsYesCannot (shadow only)

The general advice: keep variables as local as possible. Pass data in as parameters and return data out as return values. Reserve global and nonlocal for cases where they genuinely simplify the design, not as a way to avoid thinking about function interfaces.