Skip to main content

Python Variable Scope And The LEGB Rule Explained

Learn what variable scopes are all about and get familiar with the 'LEGB' rule. You will also deal with scenarios where you'll get to see the global and nonlocal keywords in action.
Updated Sep 11, 2025  · 8 min read

Seeing “name is not defined” or “local variable referenced before assignment” right after writing a few lines is frustrating. The code looks fine, yet Python disagrees. The root cause is almost always scope: where a name is visible and which object it refers to at that moment.

Once you understand Python’s name lookup—often summarized as the LEGB rule—these errors become predictable and easy to fix. In this tutorial, I will varibles and scopes in Python step by step.

What Is a Variable?

A variable is a name bound to an object. In Python, you create a binding with the assignment operator =. Names are not typed; they can point to any object (string, int, list, function, and so on).

customer_name = "Ava Torres"
order_count = 1
total_cents = 1 + 2 * 300  # evaluated first, then bound

Follow these rules and conventions when naming variables to avoid syntax errors and subtle bugs.

  • Use letters, digits, and underscores only; names cannot start with a digit.
  • Do not use keywords such as True, for, or class as names.
  • Avoid shadowing built-ins like list, dict, sum, or max.
  • Prefer snake_case for readability (PEP 8).

These examples demonstrate what not to do and the errors Python raises:

first string value = "First string"  # spaces not allowed
# SyntaxError: invalid syntax

1st_value = 10  # cannot start with a digit
# SyntaxError: invalid decimal literal

True = "yes"  # cannot assign to a keyword
# SyntaxError: cannot assign to True

How Python Scope Works (LEGB)

Scope is the region of code where a name is visible. When you use a name, Python looks it up in this order (LEGB):

  • Local: the current function’s scope.
  • Enclosing: any outer function scopes (for nested functions).
  • Global: the module’s top level (module namespace).
  • Built-in: names defined by Python in builtins (for example, len, print).

Names live in namespaces (think dictionaries mapping names to objects). Scope is about which namespaces Python consults to resolve a name at a given point in code.

Local scope

Names assigned inside a function are local to that function unless declared otherwise. They are not visible outside the function.

def show_order_id():
    order_id = 42
    print("inside function:", order_id)

show_order_id()
print("outside function:", order_id)  # NameError

Python decides scope at compile time. If a function assigns to a name anywhere in its body, all uses of that name in the function are treated as local—leading to a common UnboundLocalError when you read before assigning:

discount_rate = 0.10  # module-level (global)

def price_with_discount(amount_cents):
    print("configured discount:", discount_rate)  # looks local because of the assignment below
    discount_rate = 0.20  # assignment makes 'discount_rate' local in this function
    return int(amount_cents * (1 - discount_rate))

# UnboundLocalError: cannot access local variable 'discount_rate' where it is not associated with a value

To use the module-level name in the function, either avoid assigning to it or mark it global (covered below).

Enclosing scope (closures)

Nested functions can see names from their immediately enclosing function. To rebind such a name (not just read it), declare it nonlocal.

def make_step_counter():
    count = 0  # enclosing scope for 'increment'

    def increment():
        nonlocal count  # rebind the 'count' in the nearest enclosing function
        count += 1
        return count

    return increment

step = make_step_counter()
print(step())  # 1
print(step())  # 2

Without nonlocal, assigning to count inside increment() would create a new local name and leave the outer count unchanged.

Global scope

Names assigned at the top level of a module live in the module’s global namespace. Any function can read them. To assign to a module-level name from inside a function, declare it global.

greeting = "Hello"

def greet_city(city_name):
    print(greeting, city_name)  # reads global

def set_greeting(new_greeting):
    global greeting
    greeting = new_greeting     # rebinds global

greet_city("Nairobi")  # Hello Nairobi
set_greeting("Hi")
greet_city("Nairobi")  # Hi Nairobi

Use global sparingly. Prefer passing values as parameters and returning results to keep code testable and predictable.

Built-in scope (and a note on keywords)

The built-in scope contains names like len, print, and Exception. Avoid shadowing them, or you’ll lose access to the built-in for that scope.

list = [1, 2, 3]     # shadows the built-in 'list' constructor
list("abc")          # TypeError: 'list' object is not callable
del list             # fix by deleting the shadowing name

Keywords (such as if, for, def) are part of Python’s syntax, not the built-in namespace, and can never be used as identifiers.

Blocks, Loops, and Comprehensions

Python’s block statements and comprehensions have specific scoping behaviors that often surprise developers coming from other languages.

There is no block scope for if/for/while/with

Assignments inside these blocks affect the containing scope (function or module). Loop variables also remain defined after the loop ends.

if True:
    status = "ready"
print(status)  # "ready"

for i in range(3):
    pass
print(i)  # 2 (the last value from the loop)

Comprehensions isolate their iteration variable

List, dict, and set comprehensions have their own local scope for loop variables. The iteration variable does not leak into the surrounding scope.

numbers = [1, 2, 3]
[x for x in numbers]
print("x" in globals() or "x" in locals())  # False

Python 3.12 (PEP 709) inlined comprehensions for speed while preserving this isolation; you still get clear, non-leaking loop variables with faster execution.

Using the global Keyword

Declare a name as global inside a function when you need to rebind a module-level variable. Place the declaration near the top of the function so it’s obvious which variable you are modifying.

tax_rate = 0.08

def configure_tax(rate):
    global tax_rate
    tax_rate = float(rate)

def total_with_tax(cents):
    return int(cents * (1 + tax_rate))

configure_tax(0.10)
print(total_with_tax(1000))  # 1100

Using the nonlocal Keyword

Use nonlocal to rebind a name from the nearest enclosing function scope. This is common in closures that maintain state.

def make_accumulator(start=0):
    total = start
    def add(amount):
        nonlocal total
        total += amount
        return total
    return add

acc = make_accumulator()
print(acc(5))   # 5
print(acc(10))  # 15

Using nonlocal for a name not defined in an enclosing function is a SyntaxError. If the name is global, use global instead.

locals() and globals() in 2025

These functions are useful for inspection and debugging, not for updating variables. Starting in Python 3.13 (PEP 667), each call to locals() in a function returns an independent snapshot. Editing that snapshot does not change real locals. I confirmed this behavior by running the following snippet on Python 3.13:

def probe():
    project = "alpha"
    snap1 = locals()
    snap1["project"] = "beta"  # edits the snapshot only
    observed = project         # still "alpha"
    snap2 = locals()           # new snapshot
    return snap1["project"], observed, snap2["project"]

print(probe())  # ('beta', 'alpha', 'alpha')

Use globals() similarly for the module namespace. Prefer explicit parameters and return values over dynamic lookups when writing application code.

Common Pitfalls and How to Fix Them

These mistakes account for most scope-related errors I see in practice.

  • UnboundLocalError from assigning in a function: Python treats a name as local if it’s assigned anywhere in the function. Either move the read after the assignment, rename, or add global/nonlocal as appropriate.
  • Expecting block scope: Names assigned in if/for/while/with persist in the containing scope. Use tighter helper functions to limit scope.
  • Shadowing built-ins: Avoid names such as list, dict, sum, id, or min. Choose descriptive names like customer_list or min_allowed.
  • Forgetting nonlocal in closures: If you intend to update an outer function variable, declare it nonlocal. Otherwise you create a new local and the outer value doesn’t change.
  • Confusing keywords with built-ins: Keywords (syntax) cannot be used as names, ever. Built-ins can be shadowed but shouldn’t be.

Best Practices That Keep Scope Simple

These habits make code easier to read, test, and debug.

  • Favor small, focused functions. Define variables in the narrowest scope that works.
  • Pass data in and out via parameters and return values. Minimize module-level state.
  • Use closures intentionally. Document which outer names a nested function captures, and use nonlocal when you truly need to rebind.
  • Choose descriptive, non-conflicting names. A leading underscore (for example, _cache) signals internal use.
  • Treat locals()/globals() as read-only diagnostics, not a configuration mechanism.

Conclusion

Python resolves names by searching Local → Enclosing → Global → Built-in scopes. Understanding that order—and when to use global or nonlocal—eliminates common errors like NameError and UnboundLocalError. Keep variables in the smallest practical scope, avoid shadowing built-ins, and use closures deliberately. With that mental model, your functions behave predictably, and scope stops being a source of surprises.


Sejal Jaiswal's photo
Author
Sejal Jaiswal
LinkedIn

I have worked in various industries and have worn multiple hats: software developer, machine learning researcher, data scientist, product manager. But at the core of it all, I am a programmer who loves to learn and share knowledge!

Topics

Learn more about Python

Course

Introduction to Python

4 hr
6.6M
Master the basics of data analysis with Python in just four hours. This online course will introduce the Python interface and explore popular packages.
See DetailsRight Arrow
Start Course
See MoreRight Arrow
Related

Tutorial

Python Global Interpreter Lock Tutorial

Learn what Global Interpreter Lock is, how it works, and why you should use it.
Aditya Sharma's photo

Aditya Sharma

Tutorial

Variables in Scala

Learn about Scala's variables, rules for defining variables, different types of variables with multiple declaration and assignments along with the scope of a variable.

Olivia Smith

Tutorial

Python Lambda Functions: A Beginner’s Guide

Learn about Python lambda functions, their purpose, and when to use them. Includes practical examples and best practices for effective implementation.
Mark Pedigo's photo

Mark Pedigo

Tutorial

Python Loops Tutorial

A comprehensive introductory tutorial to Python loops. Learn and practice while and for loops, nested loops, the break and continue keywords, the range function and more!
Satyabrata Pal's photo

Satyabrata Pal

Tutorial

PEP-8 Tutorial: Code Standards in Python

With this beginner tutorial, you'll start to explore PEP-8, Python's style guide, so that you can start formatting your code correctly to maximize its readability!
Chitrank Dixit's photo

Chitrank Dixit

Tutorial

How to Document Python Code

Learn why there is a need for documenting code and best practices to do it. Further, learn to leverage the potential of the Pydoc module for documenting purposes.
Aditya Sharma's photo

Aditya Sharma

See MoreSee More