What Are the Rules for Local and Global Variables in Python?

Variable scope is one of those topics that feels simple until the moment Python raises an UnboundLocalError and you have no idea why. Understanding exactly where a variable lives — and who can see it — is foundational to writing Python that actually works the way you expect.

Every variable in Python exists somewhere. When you write x = 10, Python creates that variable in a specific namespace — a container that maps names to objects. The rules governing which code can read or modify a variable are collectively called scope rules. Python enforces these rules consistently, and once you know them, confusing bugs start making a lot more sense.

What Scope Actually Means

A variable's scope is the region of code where that variable is accessible by name. In Python, scope is determined entirely by where a variable is assigned, not where it is used. This is a critical distinction that trips up many beginners coming from other languages.

There are two primary categories to understand first: local and global.

Local Variables

A variable is local when it is assigned inside a function. It only exists for the lifetime of that function call. Once the function returns, the variable is gone. No code outside the function can access it by name.

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

result = calculate_tax(100)
print(result)       # 8.0
print(tax_rate)     # NameError: name 'tax_rate' is not defined

In this example, tax_rate and tax_amount are both local to calculate_tax. The moment you try to access tax_rate outside the function, Python raises a NameError because the name does not exist in that context.

Global Variables

A variable is global when it is assigned at the top level of a module — meaning it lives outside any function or class. Global variables exist for the entire duration of the program and are accessible from any code within the same module.

base_url = "https://api.example.com"   # global variable

def fetch_data(endpoint):
    full_url = base_url + endpoint      # reading the global is fine
    return full_url

print(fetch_data("/users"))
# https://api.example.com/users

Here, fetch_data can read base_url without any special syntax because Python looks for names in the local scope first, then the enclosing scopes, and finally the global scope. Reading a global variable from inside a function is always permitted.

Note

Scope in Python is determined at compile time based on assignments, not at runtime based on execution order. This means Python decides whether a variable is local or global before the function even runs.

The LEGB Rule

Python resolves variable names by searching through four scopes in a fixed order. This search sequence is known as the LEGB rule, named after the four scope levels it covers:

  • L — Local: The inside of the current function.
  • E — Enclosing: Any outer functions that wrap the current function (relevant for nested functions).
  • G — Global: The top-level scope of the current module.
  • B — Built-in: Python's built-in namespace, which contains names like print, len, and range.

Python walks this chain from left to right and stops as soon as it finds a match. If a name is not found in any of these four layers, a NameError is raised.

count = 100    # Global scope

def outer():
    count = 50    # Enclosing scope

    def inner():
        print(count)    # Finds 'count' in Enclosing scope first

    inner()

outer()
# Output: 50

In this example, inner() does not define count locally, so Python moves to the enclosing scope (the body of outer) and finds it there. The global count = 100 is never reached because the search stopped earlier.

Pro Tip

Memorize LEGB as a search order, not a hierarchy of importance. Python always starts from the narrowest scope (Local) and works outward. The first match wins.

The Built-in Scope

The built-in scope is the outermost layer. It contains all of Python's pre-defined names. Because it is searched last, you can shadow built-in names by defining your own variables with the same name — though doing so is almost always a mistake.

# This shadows the built-in 'len' function — don't do this
len = 42
print(len("hello"))    # TypeError: 'int' object is not callable

# Fix by deleting the local binding
del len
print(len("hello"))    # 5

The global and nonlocal Keywords

Reading a global variable from inside a function is straightforward. But what happens when you want to modify a global variable from inside a function? This is where the global keyword becomes necessary.

The global Keyword

When you assign to a variable inside a function, Python treats that variable as local to that function by default. If you want the assignment to affect the global variable instead, you must declare it with global at the top of the function.

login_count = 0    # global variable

def record_login():
    global login_count       # tell Python we mean the global one
    login_count += 1

record_login()
record_login()
record_login()

print(login_count)    # 3

Without global login_count, the line login_count += 1 would raise an UnboundLocalError. Python would see the assignment in the function, classify login_count as local, and then fail when it tries to read the local variable before it has been assigned.

Warning

Relying on global excessively is a sign that your code may need to be restructured. Functions that modify global state are harder to test, harder to reason about, and more likely to introduce bugs. In many cases, passing the value as an argument and returning a new value is a cleaner design.

The nonlocal Keyword

Nested functions introduce a third scenario. If you have a function inside a function and you want the inner function to modify a variable from the outer function's scope (not the global scope), you use the nonlocal keyword. This was introduced in Python 3 and has no Python 2 equivalent.

def make_counter():
    count = 0    # enclosing scope variable

    def increment():
        nonlocal count    # modify the enclosing variable
        count += 1
        return count

    return increment

counter = make_counter()
print(counter())    # 1
print(counter())    # 2
print(counter())    # 3

Here, nonlocal count tells Python that count refers to the variable in the enclosing make_counter function, not a new local variable and not the global scope. This pattern is commonly used in closures, where the inner function retains access to the outer function's state even after the outer function has returned.

"Namespaces are one honking great idea — let's do more of those!" — The Zen of Python

Common Mistakes and How to Avoid Them

Scope issues produce some of the most confusing error messages for Python beginners. Here are the scenarios that come up repeatedly.

The UnboundLocalError Trap

This is the most common scope-related error. It happens when a function contains an assignment to a variable with the same name as a global, which causes Python to treat all references to that name within the function as local — including the ones that appear before the assignment.

total = 500

def add_bonus(amount):
    total = total + amount    # UnboundLocalError
    return total

add_bonus(100)

Python sees the assignment total = ... on the left side and marks total as local for the entire function. When it tries to evaluate the right side (total + amount), the local total hasn't been assigned yet. The fix is either to declare global total at the top of the function, or better yet, pass total as a parameter:

def add_bonus(total, amount):
    return total + amount

result = add_bonus(500, 100)
print(result)    # 600

Mutable Globals Behave Differently

Here is a subtlety that surprises many developers. You can modify the contents of a mutable global object (like a list or dictionary) from inside a function without using global, because you are not reassigning the name — you are mutating the object it points to.

active_users = []    # mutable global

def add_user(username):
    active_users.append(username)    # mutating, not reassigning — no 'global' needed

add_user("alice")
add_user("bob")
print(active_users)    # ['alice', 'bob']

However, the moment you try to reassign the name itself (pointing it at a new object), you need global:

active_users = []

def reset_users():
    global active_users
    active_users = []    # reassignment requires 'global'

reset_users()
Pro Tip

A good mental model: global is needed when the variable name on the left side of an = sign should refer to the module-level variable. Calling a method on an existing object (like .append()) is not the same as assignment.

Functions Are Closures, Not Snapshots

When a nested function references a variable from an enclosing scope, it captures the variable itself — not a copy of its value at the time the function was defined. This leads to the classic loop closure bug:

# Common mistake: all functions end up using the same 'i'
functions = []
for i in range(3):
    def f():
        return i
    functions.append(f)

print([fn() for fn in functions])    # [2, 2, 2] — probably not what you wanted

# Fix: capture the current value as a default argument
functions = []
for i in range(3):
    def f(x=i):    # x is a new local variable holding the current value of i
        return x
    functions.append(f)

print([fn() for fn in functions])    # [0, 1, 2]

The default argument trick works because default argument values are evaluated at function definition time, not at call time. This gives each function its own copy of the loop variable's value at that moment.

Key Takeaways

  1. Scope is determined by assignment location: If a variable is assigned inside a function, Python treats it as local to that function for the entire function body — even lines above the assignment.
  2. Python searches scopes in LEGB order: Local, then Enclosing, then Global, then Built-in. The first match found is used, and the search stops there.
  3. Use global to modify a global variable from inside a function: Reading a global is always allowed, but writing to it requires the global declaration — otherwise Python creates a new local variable instead.
  4. Use nonlocal for nested functions: When an inner function needs to modify a variable from an outer function (not the global scope), nonlocal is the correct tool.
  5. Mutating a mutable object is not the same as reassigning a name: Methods like .append() or .update() modify the object in place and do not require global; pointing the name at a new object does.

Variable scope in Python is consistent and logical once you see the pattern behind it. The LEGB rule gives you a single mental model that explains nearly every scope-related behavior in the language. Keep it in mind the next time Python raises an UnboundLocalError, and the cause will become clear almost immediately.

back to articles