Python Scope โ The LEGB Rule Explained Properly
Everything you actually need to know about variable scope in Python โ Local, Enclosing, Global, Built-in resolution order, the global and nonlocal keywords, closures, and the exact UnboundLocalError you'll hit while learning this.
Last Updated
March 2026
Read Time
20 min
Level
Beginner
What is Scope in Python?
Scope answers one specific question: when your code says a name like total, which actual variable does Python mean? The same name can exist in several completely separate places at once โ a total inside one function, a different total inside another, and a third total sitting at the top level of your script โ and scope is the set of rules Python follows to figure out, for any given line of code, exactly which one you're talking about.
Python resolves this using a specific, ordered search called the LEGB rule โ Local, Enclosing, Global, Built-in. When your code references a name, Python checks these four scopes in that exact order and uses the first match it finds. It's not a vague heuristic; it's a precise, deterministic algorithm that runs every single time a name is looked up, and once you actually know the order, a huge number of 'why isn't my variable updating' bugs stop being mysterious.
Here's the specific moment scope stopped being abstract for me: I had a function that read if count > threshold: where count was meant to be a variable the function itself incremented on every call, tracking state across multiple calls. It never worked โ count stayed at zero forever, no matter how many times I called the function. The bug wasn't in the logic. It was that count += 1 inside the function silently created a brand-new local variable named count, completely separate from the global one I thought I was updating, and Python never told me this was happening โ it just quietly did it, exactly per the LEGB rule, and I hadn't yet learned the rule well enough to see it coming.
This guide walks through all four LEGB scopes precisely, the global and nonlocal keywords that let you deliberately reach outside a function's local scope, how closures capture enclosing-scope variables, and the exact UnboundLocalError traceback that's almost always the direct symptom of a scope misunderstanding.
History โ How Python's Scoping Rules Evolved
Python's scoping model has genuinely changed shape over the decades, not just gained new keywords โ an important thing to know if you ever read code or documentation written for an earlier version.
- โถ
1991 โ Python 0.9.0 โ Basic local and global scope existed from Python's earliest release, along with the global keyword, letting a function explicitly declare it intends to modify a module-level variable rather than create a new local one.
- โถ
Early-to-mid 1990s โ No Enclosing Scope Yet โ In Python's earliest versions, nested functions could NOT read variables from an enclosing function's local scope at all โ only from local and global/built-in scopes. A function defined inside another function was, for scoping purposes, almost as isolated as a top-level function.
- โถ
2001 โ Python 2.1 (PEP 227) โ Introduced genuine nested scopes, formally adding the 'E' (Enclosing) to what would eventually be called the LEGB rule. This single change is what made real closures possible in Python, and it directly enabled the decorator pattern years before decorators had their own dedicated @ syntax.
- โถ
2008 โ Python 3.0 (PEP 3104) โ Introduced the nonlocal keyword, finally giving nested functions a clean, explicit way to MODIFY (not just read) a variable in an enclosing function's scope โ something Python 2 genuinely had no clean syntax for at all, forcing awkward workarounds like wrapping a value in a mutable list.
- โถ
2008 โ Python 3.0, Comprehension Scoping Change โ List comprehensions gained their own private scope in Python 3, meaning the loop variable inside a comprehension no longer leaks into the enclosing scope the way it did in Python 2 โ a subtle but genuinely important scoping fix that eliminated a class of accidental variable-overwriting bugs.
- โถ
2026 โ Current state โ The LEGB rule, global, and nonlocal remain exactly as they've been since Python 3.0. This is one of the most stable areas of the language's core semantics, precisely because getting scoping rules right is foundational โ changing them later would silently break enormous amounts of existing code.
Key Characteristics of Python Scope
Here are the 11 things worth knowing precisely about how Python scope actually behaves โ not the loose version, the exact one:
Python resolves a name by checking Local, then Enclosing, then Global, then Built-in scope, in that exact order, stopping at the first match found. This search happens for every single name lookup.
Every function call gets a fresh, private local namespace. Variables assigned inside a function are local to that function by default and don't exist outside it.
Unlike many languages, if, for, and while blocks in Python do NOT create their own scope. A variable assigned inside an if block or for loop is accessible after that block ends, within the same function or module.
If a name is assigned anywhere inside a function body, Python treats it as local for the ENTIRE function โ even on lines before that assignment. This is a compile-time decision, not a runtime one, and it's the root cause of most scope-related bugs.
Writing global count inside a function tells Python that count refers to the module-level variable, not a new local one โ allowing the function to genuinely modify the global value.
Writing nonlocal count inside a nested function tells Python that count refers to a variable in the nearest enclosing function's scope, not the module-level global or a new local โ enabling genuine modification of that enclosing variable.
You can freely READ a variable from an enclosing or global scope inside a function without any special keyword. global and nonlocal are only required when you intend to REASSIGN that name, not merely read it.
A nested function that references an enclosing function's variable keeps access to that variable even after the enclosing function has finished executing โ this is the mechanism behind Python closures and decorators.
Global scope is actually per-module, not truly program-wide. A variable defined at the top level of one .py file is that module's global scope, separate from another module's globals unless explicitly imported.
Names like len, print, range, and Exception live in the outermost Built-in scope, checked last โ which is also why shadowing them (naming a variable list, for instance) silently hides the built-in within that scope.
Calling a method that mutates a mutable object in place (like list.append()) doesn't require global or nonlocal, because you're not reassigning the name itself โ only reassignment triggers Python's local-by-default rule.
How Python Resolves a Variable Name โ The LEGB Flowchart
Every single time your code references a variable name, Python runs through this exact search, in this exact order, checking one scope after another until it finds a match โ or runs out of scopes and raises NameError.
Code Execution Flow โ from source to output
Key insight: this search stops at the first match. If a variable named list exists locally inside your function, Python uses that local variable every time list is referenced in that function โ it never even gets to Built-in scope to check Python's actual list type, silently shadowing it for the rest of that function's execution.
How Scope Actually Works โ Local-by-Default, global, and nonlocal
Three things separate someone who's memorised 'LEGB' as an acronym from someone who actually understands scope: knowing exactly when Python decides a variable is local, understanding precisely what global and nonlocal each do (and don't do), and recognising the specific traceback these misunderstandings produce.
โ๏ธ The Rule That Trips Up Almost Everyone: Assignment Makes It Local
This is the single most important, most surprising rule in Python scoping: if a name is assigned anywhere inside a function's body, Python treats that name as local to the entire function โ including on lines that come before the assignment. Python decides this at compile time, by scanning the whole function body for assignment statements, before the function ever actually runs. This is why count += 1 inside a function, intending to modify a module-level count, doesn't work as expected: count += 1 is really count = count + 1, which is an assignment, which makes Python treat count as local for that entire function โ and then the read on the right-hand side (count, before the +) fails, because that local count hasn't been given a value yet at the point it's being read.
๐ฅ UnboundLocalError โ The Exact Error This Produces
Exactly this scenario raises UnboundLocalError: local variable 'count' referenced before assignment. This error message is precise and honest, but it confuses beginners because the variable clearly exists โ just not the one they meant. Python correctly identifies count as local (because of the assignment somewhere in the function), and then correctly points out that this specific local variable has no value yet at the line where you're trying to read it. The fix isn't about the variable existing or not; it's about telling Python which scope you actually mean.
๐ global โ Reaching Out to Module-Level Scope
Adding global count as the first line inside the function tells Python explicitly: 'when this function uses the name count, it means the module-level variable, not a new local one.' With that declaration in place, count += 1 genuinely modifies the global value, and every subsequent call to the function sees the updated number. global must be declared before the variable is used in that function, and it applies only within that specific function's body โ it doesn't leak into any other function.
๐ช nonlocal โ Reaching Out to an Enclosing Function's Scope
nonlocal solves the identical problem one level down: a nested function that needs to modify a variable belonging to the function it's defined inside, rather than the module-level global or its own new local variable. This is exactly what a counter-generator closure pattern needs โ an inner function incrementing a counter that lives in its enclosing function's scope, persisting across multiple calls to that inner function, without that counter ever touching module-level global scope at all.
Simple rule to remember: reading a variable from an outer scope needs no special keyword at all. Reassigning it does โ global for module-level variables, nonlocal for enclosing-function variables. Mutating a mutable object in place (like my_list.append(x)) is a read of the reference plus a method call, not a reassignment, so it needs neither keyword.
Local vs Enclosing vs Global vs Built-in โ Key Differences
Each of the four LEGB scopes has a genuinely different lifetime, visibility, and typical use. This table lines them up directly.
Variable Scoping โ Python vs Other Languages
Python's function-based, block-transparent scoping is genuinely different from block-scoped languages, and this difference causes real confusion for developers moving between them in either direction.
Here's my honestly opinionated take: Python's decision to make if and for blocks NOT create their own scope is one of its more underrated design choices, and I say that as someone who's been genuinely bitten by it too. It means you don't need to pre-declare a variable above a loop just to use its final value after the loop ends, the way you sometimes do in block-scoped languages. The trade-off is that it makes the 'accidentally shadow the outer variable' mistake slightly easier to make inside functions โ but honestly, that trade-off favours Python's broader goal of minimal ceremony over strict, block-by-block variable isolation.
Advantages and Disadvantages of Python's Scoping Model
Python's approach to scope is deliberately simple compared to many languages, and that simplicity has real costs alongside its real benefits.
Scope Architecture โ Namespaces Nested Inside Each Other
The diagram below shows how Python's four scopes physically nest inside each other, from the innermost function call out to the Python interpreter's own built-in namespace.
Scope in Practice โ From UnboundLocalError to Working global and nonlocal
Here's the exact broken version first, its precise error, and then the two correct fixes โ one using global, one using nonlocal inside a closure.
count = 0
def increment():
count += 1 # intends to modify the global count
print(count)
increment()Output
Traceback (most recent call last): File "broken_scope.py", line 7, in <module> increment() File "broken_scope.py", line 4, in increment count += 1 UnboundLocalError: cannot access local variable 'count' where it is not associated with a valueThe fix, using global:
count = 0
def increment():
global count
count += 1
print(count)
increment()
increment()
increment()Output
1 2 3And the closure version, using nonlocal instead โ a self-contained counter with no module-level global involved at all:
def make_counter():
count = 0 # enclosing-scope variable
def increment():
nonlocal count
count += 1
return count
return increment
counter_a = make_counter()
counter_b = make_counter()
print(counter_a()) # 1
print(counter_a()) # 2
print(counter_b()) # 1 โ completely independent from counter_aOutput
1 2 1Practice This Code โ Live Editor
Line-by-Line Explanation
- โถ
count += 1withoutglobalโ Becausecountis assigned insideincrement(), Python treats it as local for the whole function, and reading it on the right side of+=before it has a local value raisesUnboundLocalError. - โถ
global countโ Declares, before any use, thatcountinside this function refers to the module-level variable. Nowcount += 1correctly reads and writes that same global variable. - โถ
count = 0insidemake_counter()โ This isincrement()'s enclosing scope variable, not global โ it belongs tomake_counter()'s own local namespace. - โถ
nonlocal countinsideincrement()โ Declares that this inner function'scountrefers to the enclosing function'scount, not a new local one and not the module-level global (there isn't one here anyway). - โถ
counter_aandcounter_bโ Two separate calls tomake_counter()create two entirely independent enclosing scopes, each with its own privatecountโ which is exactly why callingcounter_b()starts back at 1, unaffected bycounter_a's calls.
Where Scope Understanding Actually Matters โ Real-World Applications
This isn't abstract theory โ scope rules shape how real, working Python code is actually structured across many domains:
- โถ
๐จ Decorators โ Nearly every decorator relies on a nested function closing over variables from its enclosing function, exactly the Enclosing scope mechanism covered here โ understanding LEGB is a genuine prerequisite for writing decorators confidently rather than by rote imitation.
- โถ
๐ญ Factory Functions Generating Customised Behaviour โ Functions that return other functions, each configured differently (like make_counter() above, or a make_multiplier(factor) that returns a function multiplying by that specific factor), depend entirely on closures capturing enclosing-scope state correctly.
- โถ
โ๏ธ Configuration and Application State ManagementGlobal-scope constants and configuration values (API_URL = '...', DEBUG = False) are extremely common at the top of real Python modules, and understanding exactly when a function needs global to modify them (versus just reading them, which needs nothing special) is a genuinely everyday skill.
- โถ
๐งต Thread-Safe State and Avoiding Shared Mutable Globals โ Recognising that module-level global variables are shared across every part of a program that imports that module is exactly what motivates safer patterns like passing state explicitly as function arguments instead, especially in multi-threaded code.
- โถ
๐งช Test Fixtures and Mocking โ pytest fixtures and monkeypatching frequently rely on precise understanding of scope to correctly override or restore global and module-level state during tests without leaking changes between test cases.
- โถ
๐ฅ๏ธ GUI Callback and Event Handler Design โ GUI frameworks often need callback functions defined inside a class or setup function that need to modify state defined in an enclosing scope โ exactly the nonlocal use case, common in Tkinter-based applications.
- โถ
๐ Debugging 'My Variable Isn't Updating' Bugs โ An enormous share of real-world Python debugging sessions, especially for less experienced developers, end up tracing back to exactly this: a function silently creating a new local variable instead of modifying the outer one the developer intended.
- โถ
๐ฆ Module Design and Namespace Isolation โ Understanding that global scope is per-module, not program-wide, is essential for designing multi-file Python projects where different modules need their own independent top-level state without stepping on each other.
Why Scope Deserves Real, Deliberate Study
Scope is one of those topics that seems simple in a two-paragraph tutorial summary and then quietly causes real, recurring confusion in actual code. Here's why it's worth the deeper look:
- โถ
๐ It's a Genuinely Common Source of Silent Bugs โ Unlike a syntax error, a scope mistake often doesn't crash โ it just quietly produces the wrong result, like a counter that never actually increments, which can be far harder to notice and debug than a loud traceback.
- โถ
๐ผ It's Directly Tested in Interviews โ 'What will this code print?' questions involving nested functions, global, and nonlocal are a genuinely common way interviewers test whether a candidate understands Python at a level beyond copy-pasted syntax.
- โถ
๐ง It's a Prerequisite for Decorators and Closures โ You genuinely cannot understand how decorators work, or confidently write your own, without a solid grasp of enclosing scope and how nested functions capture outer variables.
- โถ
๐ง It Reinforces a Precise, Useful Mental Model โ The LEGB search order is a genuinely reusable mental tool โ once it's solid, reading unfamiliar code with nested functions and closures becomes dramatically less mysterious.
- โถ
๐๏ธ It Shapes Good Architecture Decisions โ Knowing when reaching for global is a code smell (heavy reliance on shared mutable global state) versus when it's a reasonable, deliberate choice (a small script's simple configuration) is a real design judgment call this understanding enables.
- โถ
๐ฎ๐ณ It's a Standard Deep-Dive Topic Alongside Functions in Indian Curricula โ Scope, alongside *args/**kwargs and recursion, is typically given dedicated coverage in Indian engineering college Python courses and interview-prep material, precisely because it reveals genuine conceptual understanding.
Namespaces vs Scope โ A Distinction Worth Making Precisely
These two terms get used almost interchangeably in casual conversation, but they describe genuinely different things, and being precise about the difference sharpens your understanding of both.
- โถ
A namespace โ is the actual mapping from names to objects โ practically, a dictionary Python maintains behind the scenes, associating variable names with the objects they currently refer to. Every module, every function call, and every class has its own separate namespace.
- โถ
Scope โ is the RULE for which namespaces are visible, and in what order, from a particular point in your code. Scope is what tells Python which namespace (or namespaces) to actually search when resolving a name.
- โถ
How they relate โ LEGB describes the specific sequence of namespaces โ Local namespace, then Enclosing function's namespace, then Global (module) namespace, then Built-in namespace โ that scope rules make visible and searchable, in that order, from any given line of code.
- โถ
Practical tools for inspecting namespaces โ locals() returns a dictionary of the current local namespace; globals() returns the current module's global namespace; these are genuinely useful for debugging exactly what names are visible and what values they currently hold at a specific point in your code.
Python Scope โ Interview Questions
These come up regularly in Python interviews, particularly around closures, global state, and predicting output for nested-function code.
Practice Questions โ Test Your Understanding
Work through these before checking the answers. Correctly predicting when a scope error happens โ not just what value gets printed โ is the real test here.
1. What is the output of: x = 'global' \n def show(): print(x) \n show()
Easy2. What happens when you run: x = 'global' \n def show(): print(x); x = 'local' \n show()?
Medium3. Trace this: for i in range(3): pass \n print(i) โ what does it print, and why doesn't this raise an error?
Easy4. What is the output of: def outer(): x = 'outer' \n def inner(): x = 'inner'; print(x) \n inner() \n print(x) \n outer()
Medium5. What would change in the previous question if nonlocal x were added as the first line inside inner()?
Hard6. What is the output of: list = [1, 2, 3] \n print(list(range(3)))
Hard7. What does make_multiplier(3) return, functionally, given: def make_multiplier(factor): def multiply(x): return x * factor \n return multiply โ and what does make_multiplier(3)(10) evaluate to?
Medium8. Why does globals()['x'] = 'changed' work to modify a global variable x from inside a function, even without the global keyword?
HardConclusion โ LEGB Is the Quiet Rule Behind Almost Every 'Why Isn't This Updating' Bug
Scope is one of those Python topics that rarely gets its own dedicated attention early on, and then quietly turns out to be the actual explanation behind a real share of the confusing bugs beginners hit in their first few months. UnboundLocalError, a counter that never increments, a variable that seems to 'reset' unexpectedly inside a function โ these almost always trace back to the same underlying mechanism: Python's local-by-default assignment rule, working exactly as designed, just not as the developer expected.
If you're a complete beginner, the priority is internalising the LEGB search order and the local-by-default assignment rule until predicting scope behaviour becomes automatic, not something you have to consciously reason through every time. If you're writing real, shared code, the priority shifts to judgment: recognising when reaching for global is a reasonable, deliberate choice versus when it's a sign that state should be passed explicitly as a function argument instead.
The habit worth carrying forward: whenever a function seems to be silently ignoring an update to a variable you expected it to change, the very first question to ask isn't 'is my logic wrong' โ it's 'does this variable name get assigned anywhere inside this function, and did I mean for that to create a new local one.' That single question resolves an outsized share of Python's most confusing-looking bugs almost instantly.
Scope is a search order, not a mystery. Know the four letters, know exactly what triggers 'local,' and know precisely which keyword reaches outside it โ everything else about reading and writing variable-heavy Python code gets noticeably clearer from there. ๐