Python: Scope and the LEGB Rule (Local, Enclosing, Global)

Python scope decides where a variable name is visible and where it is invisible. This post unpacks the LEGB rule (Local, Enclosing, Global, Built-in), the one mental model that explains how Python finds the right variable, plus the two keywords (global and nonlocal) that let you reach across scope lines. Every example below was run on Python 3.14.6.

“Namespaces are one honking great idea, let’s do more of those!”

Tim Peters, The Zen of Python (PEP 20)

Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 14 minutes

Here is code that confuses almost every beginner at least once. It looks like it should add one to a counter. Instead Python throws an error and refuses to run it.

📄 mystery.py: why does this refuse to run?

count = 0

def increment():
    count += 1   # raises UnboundLocalError

try:
    increment()
except UnboundLocalError as e:
    print(f"Error: {e}")

▶ Output

Error: cannot access local variable 'count' where it is not associated with a value

You defined count outside the function. You try to bump it up inside. Python raises an error the instant that line runs, and the strange part is that the mistake was locked in earlier, while Python was still reading your function. The reason is scope, the set of rules Python uses to decide which count you actually mean. By the end of this post that error message will look obvious, and you will know two clean ways to fix it.

Think of scope like rooms in a house. A conversation in the kitchen cannot be heard in the bedroom. Each function Python runs gets its own room (the proper name is a namespace), and the names you create in that room stay in that room. When the function finishes, Python clears the room out. So why does the counter above blow up instead of just reading the count from the hallway? That is exactly what the LEGB rule explains.

The Mental Model: LEGB Layers

Python scope resolution follows one fixed search order. When Python encounters a variable name, it checks these layers in turn:

1. Check LocalNot foundNot foundNot foundStill not foundPython looks upa variable nameL LocalCurrent functionE EnclosingOuter functionG GlobalModule levelB Built-inprint, len, rangeNameError raisedPython Scope: How the LEGB Rule Resolves a Variable Name, Local to Built-in

The diagram shows Python’s LEGB rule. When you use a variable name, Python searches four scopes in a fixed order: Local (inside the current function), Enclosing (inside any outer function wrapped around it), Global (the module level, that is, the top of your file), and Built-in (names Python ships with, like print and len). The search stops at the very first match, so a local name quietly hides a global one with the same spelling. This single lookup order explains almost every “variable not found” or “wrong value” surprise you will ever hit with Python variable scope.

  1. Local: inside the current function
  2. Enclosing: inside any enclosing (outer) functions
  3. Global: at the module level (top of the file)
  4. Built-in: Python’s built-in names (len, print, range, and so on)

Here is the everyday version. Imagine you lose your keys and search your pockets first, then your bag, then the whole house, and finally you phone the neighbour who keeps a spare. You always check the closest place first and stop the instant you find them. Python does the same with names: pockets are Local, bag is Enclosing, house is Global, the neighbour is Built-in. It searches inside out, stops at the first match, and only if every scope comes up empty does it give up with a NameError.

Local Scope

📄 local_scope.py: variables made inside a function stay inside it

def calculate():
    result = 42    # local to calculate()
    tax = 0.18     # also local
    print(f"Inside: result = {result}")

calculate()

try:
    print(result)  # NameError: result does not exist out here
except NameError as e:
    print(f"Outside: {e}")

▶ Output

Inside: result = 42
Outside: name 'result' is not defined

What happened here: result and tax are born the moment calculate() runs and they are gone the moment it returns. They live in the local scope, that private room we talked about. Out at the module level, result was never defined, so Python walks the whole LEGB chain, finds nothing, and raises NameError. That error is Python telling you the name simply does not exist anywhere it is allowed to look.

Enclosing Scope

Functions can live inside other functions, and when they do, a new layer appears between Local and Global. Think of a small cabin built inside a big hall: someone in the cabin can hear announcements made in the hall, but people in the hall cannot hear a whisper inside the cabin. The inner function is the cabin, the outer function is the hall.

📄 enclosing.py: a nested function can read its outer function’s variables

def outer():
    message = "Hello from outer"  # Enclosing scope for inner()

    def inner():
        print(message)  # Found in enclosing scope

    inner()

outer()

▶ Output

Hello from outer

inner() has no local message of its own, so Python takes one step out to the enclosing scope, the outer() function, and finds message sitting right there. That outward step is the E in LEGB. This exact behaviour is what makes closures possible, and we cover those properly in the closures tutorial.

Global Scope

Global scope is the top level of your file, outside every function. It works like the notice board in an office lobby: anyone walking past can read it, but you are not supposed to scribble on it from your desk. Say a developer named Aviraj pins the app name and version on that board; every function in his file can read them without asking.

📄 global_scope.py: names defined at the top of the file

APP_NAME = "TechnoScripts"  # Global scope
VERSION = "2.0"

def show_info():
    # Can READ global variables without any special syntax
    print(f"{APP_NAME} v{VERSION}")

show_info()

def show_all_globals():
    # You can see all global names
    relevant = {k: v for k, v in globals().items() if not k.startswith("_")}
    for name, value in relevant.items():
        print(f"  {name} = {value}")

show_all_globals()

▶ Output

TechnoScripts v2.0
  APP_NAME = TechnoScripts
  VERSION = 2.0
  show_info = <function show_info at 0x...>
  show_all_globals = <function show_all_globals at 0x...>

What happened here: show_info() reads APP_NAME and VERSION without any special syntax at all. Reading a global from inside a function is free. The globals() call just hands you a dictionary of every name living at the module level, which is a handy way to see the G scope with your own eyes. The two function entries print with a memory address that changes on every run, so we shortened it to 0x... above; yours will show some other hex value. The trouble starts only when you try to reassign a global from inside a function, and that is exactly the mystery we opened with.

Built-in Scope

📄 builtin_scope.py: the names Python gives you for free

import builtins

# These are always available; they live in the built-in scope
print(len("Python"))       # len is built-in
print(type(42))            # type is built-in
print(list(range(5)))      # range, list are built-in

# See all built-in names
builtin_names = [name for name in dir(builtins) if not name.startswith("_")]
print(f"Number of built-in names: {len(builtin_names)}")
print(f"First 10: {builtin_names[:10]}")

▶ Output

6
<class 'int'>
[0, 1, 2, 3, 4]
Number of built-in names: 151
First 10: ['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BaseExceptionGroup', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError']

What happened here: Built-in scope is the outermost layer, the neighbour with the spare key. If a name is not found in Local, Enclosing, or Global, Python looks here last. That is why print, len, and range just work without a single import. The exact count of built-in names (151 on Python 3.14.6) shifts a little between versions, so treat that number as “roughly 150”, not a constant to memorise.

The global Keyword

Sometimes a function genuinely needs to update that lobby notice board, not just read it. The global keyword is the written permission slip: it tells Python that when this function says counter, it means the shared module-level one, not a fresh private copy.

📄 global_keyword.py: changing a global from inside a function

counter = 0

def increment():
    global counter   # tell Python: I mean the global counter
    counter += 1

increment()
increment()
increment()
print(f"Counter: {counter}")

▶ Output

Counter: 3

What happened here: The line global counter tells Python that counter inside the function is the one from the module level, not a fresh local. Drop that line and counter += 1 turns into “make a new local counter, then read it before it has a value”, which is the UnboundLocalError from the opening mystery.

Reach for it rarely. Global mutable state is hard to test and hard to trace, because any function anywhere can quietly change it. Most of the time, passing a value in as a parameter and returning the result is cleaner. The global keyword is for the few cases where a single shared counter or flag genuinely earns its keep.

The nonlocal Keyword

Picture a tea stall owner named Anvi who keeps one tally sheet behind the counter. Her helper’s only job is to add a tick to that same sheet for every customer. The helper never starts a private sheet of his own; he updates Anvi’s. That is what nonlocal does: it lets a nested function update a variable that lives in the function wrapping it.

📄 nonlocal_keyword.py: changing a variable in the enclosing function

def make_counter():
    count = 0

    def increment():
        nonlocal count  # reach the count in the enclosing scope
        count += 1
        return count

    return increment

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

▶ Output

1
2
3

What happened here: nonlocal does for the enclosing scope what global does for the module scope. It tells Python “modify the count in the outer function, do not make a new local one”. Each call to the returned increment remembers and bumps the same count, which is why you see 1, 2, 3 instead of 1, 1, 1. That little memory is the heart of a closure.

Step-by-Step Resolution Trace

📄 trace.py: watch LEGB pick a winner

x = "global"

def outer():
    x = "enclosing"

    def inner():
        x = "local"
        print(f"inner sees: {x}")  # Local has x, so Local wins

    inner()
    print(f"outer sees: {x}")      # outer's own local x

outer()
print(f"module sees: {x}")         # global x, untouched

print()

# This time inner2 has no x of its own
def outer2():
    x = "enclosing"

    def inner2():
        print(f"inner2 sees: {x}")  # no local x, so Enclosing wins

    inner2()

outer2()

▶ Output

inner sees: local
outer sees: enclosing
module sees: global

inner2 sees: enclosing

The resolution trace:

  1. inner() has its own x = "local" → found in Local scope, done.
  2. outer() has its own x = "enclosing" → each function has its own scope.
  3. Module level has x = "global" → untouched by the function calls.
  4. inner2() has no local x → checks Enclosing (outer2) → found "enclosing".

The Mystery Solved

📄 mystery_explained.py: why count += 1 fails without global

count = 0

def increment():
    # Python reads this as "count = count + 1"
    # the assignment makes Python treat count as LOCAL
    # but the right side reads count before it has a local value
    # result: UnboundLocalError
    count += 1

# fix one: use global (or better, pass it in as a parameter)
def increment_safe():
    global count
    count += 1

# fix two, the cleaner one: no global state at all
def increment_pure(current):
    return current + 1

value = 0
value = increment_pure(value)
value = increment_pure(value)
print(f"Pure result: {value}")

▶ Output

Pure result: 2

Here is the part that catches everyone. Python decides whether a name is local or global at compile time, while it is parsing the function, not while it is running. If there is an assignment to count anywhere in the function body, even on the last line, Python tags count as local for the whole function. So count += 1 means “read the local count, add one, store it back”, and on that very first read there is nothing to read yet. The local tag was stamped at compile time, so the UnboundLocalError is already guaranteed before the function is ever called; it just surfaces the moment that line executes.

Common Misconceptions

Misconception 1: “Functions can’t access global variables”

They can read them freely. They just can’t reassign them without the global keyword. Reading a global is fine: print(config). Reassigning is the problem: config = new_config.

Misconception 2: “global makes a variable available everywhere”

No. global tells one specific function to use the global version of a variable. Other functions still need their own global declaration if they want to modify it.

Misconception 3: “Scope is determined at runtime”

Python scope is settled at compile time, when Python parses the function. If there is an assignment to a name anywhere in the function, that name is local for the entire function, including every line that runs before the assignment. This is the root cause of the UnboundLocalError we kept circling back to.

Best Practices

  • DO use function parameters and return values instead of global variables
  • DO use ALL_CAPS naming for global constants: MAX_RETRIES = 3
  • DO keep functions self-contained, so everything they need arrives as parameters
  • DON’T use global unless absolutely necessary (it makes code hard to test)
  • DON’T shadow built-in names: avoid list = [1, 2, 3] or id = 42
  • DON’T shadow outer variables by accident, so use distinct names in nested functions

Conclusion

Python scope comes down to one rule, LEGB: Local first, then Enclosing, then Global, then Built-in. Any assignment inside a function makes that name local, settled at compile time. The global and nonlocal keywords let you reach back out when you truly need to, though passing parameters and returning values is usually the cleaner path. Get LEGB into your bones and the two bugs that haunt every beginner, UnboundLocalError and accidental shadowing, stop being mysteries.

Next up: Lambda Functions, the tiny anonymous functions you write in a single line. And if you want the full roadmap from Python basics all the way to AI/ML, browse the Python + AI/ML tutorial series home.

Practice Exercises

  1. Exercise 1: Create same-named variables in local and global scope. Print each.
  2. Exercise 2: Use global to modify a counter. Explain why this is bad practice.
  3. Exercise 3: Demonstrate LEGB with nested functions. Use nonlocal.

Frequently Asked Questions

What is the LEGB rule in Python?

LEGB stands for Local, Enclosing, Global, Built-in, the order Python searches for a variable name. When you use a variable, Python checks the local function first, then any enclosing functions, then the module level, then built-in names like len and print. It stops at the first match. That lookup order is the core of Python scope.

What causes UnboundLocalError in Python?

When you assign to a variable inside a function, Python marks it as local for the entire function. If you try to read it before the assignment runs, you get UnboundLocalError. The classic trigger is count += 1 on a global counter: Python sees the assignment, decides count is local, then fails on the read.

What does the global keyword do?

It tells Python that a variable name inside a function refers to the global (module-level) variable, not a new local one. Without it, any assignment inside a function creates a local variable.

What is the difference between global and nonlocal?

global refers to module-level variables. nonlocal refers to variables in the enclosing function (for nested functions). Both allow modification of outer-scope variables from inside a function.

Why should I avoid global variables?

Global mutable state makes code harder to understand, test, and debug. Any function can change a global variable, making it hard to track what value it holds at any point. Prefer passing values as function parameters and returning results.

Can I shadow built-in names like list or len?

Yes, but you shouldn’t. list = [1, 2, 3] overwrites the built-in list() function in that scope. After that, list('abc') fails because list is now a list object, not the constructor. Use distinct names like items or my_list.

Interview Questions on Python Scope

The same ideas as they show up in real interviews, framed as scenarios you can practice out loud.

Q: You call a function and it crashes with UnboundLocalError: cannot access local variable ‘total’, yet total is clearly defined at the top of the file. What do you check first?

Search the function body for any assignment to total, including total += x or total = ... on a later line. One assignment anywhere in the body makes Python tag total as local for the whole function at compile time, so even a read on an earlier line fails. The clean fix is to pass total in as a parameter and return the new value; global total also works but couples the function to shared state.

Q: What is the difference between a namespace and a scope?

A namespace is the actual mapping from names to objects, essentially a dictionary, which is why globals() returns a dict. A scope is the region of code from which a namespace can be reached directly, without any prefix. So the module namespace holds your global names, and global scope is every place in the code where those names are visible by the LEGB lookup.

Q: You create three functions in a loop, each meant to print its own loop number, but all three print the last value. Why, and how do you fix it?

The nested functions do not copy the loop variable; they look it up in the enclosing scope at call time, and by then the loop has finished, leaving the variable at its final value. All three functions share the same single variable. The standard fix is a default argument, def show(i=i), because default values are evaluated once when each function is defined, freezing that iteration’s value.

Q: Can nonlocal reach a module-level variable? What happens if no enclosing function defines the name?

No. nonlocal only binds to a variable in an enclosing function’s scope, never the module level; for that you need global. If no enclosing function has that name, Python raises a SyntaxError saying no binding for the nonlocal variable was found, and it does so at compile time, before any code runs.

Q: Do if blocks and for loops create their own scope in Python?

No, and this surprises people coming from Java or C. Only modules, functions, classes, and comprehensions create new scopes in Python. A variable assigned inside an if block or a for loop belongs to the surrounding function or module, which is why the loop variable is still accessible after the loop ends.

Q: How would you inspect the current namespaces while debugging a scope problem?

Call globals() for the module namespace, locals() for the current local names, and dir(builtins) after import builtins for the built-in layer. Inside a function, treat locals() as a read-only snapshot: printing it is fine, but writing to the returned dict is not a reliable way to change local variables.

Want more? the official Python documentation documents everything this post could not fit.

Previous: Python Function Arguments: Default, Keyword, *args, **kwargs

Next: Python: Lambda Functions, Anonymous Functions and When to Use

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *