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.
Table of Contents
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:
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.
- Local: inside the current function
- Enclosing: inside any enclosing (outer) functions
- Global: at the module level (top of the file)
- 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:
inner()has its ownx = "local"→ found in Local scope, done.outer()has its ownx = "enclosing"→ each function has its own scope.- Module level has
x = "global"→ untouched by the function calls. inner2()has no localx→ 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
globalunless absolutely necessary (it makes code hard to test) - DON’T shadow built-in names: avoid
list = [1, 2, 3]orid = 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
- Exercise 1: Create same-named variables in local and global scope. Print each.
- Exercise 2: Use
globalto modify a counter. Explain why this is bad practice. - 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.
Related Posts
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

No comment