๐Ÿงญ Python

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:

๐Ÿ”
LEGB Search Order

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.

๐Ÿ“ฆ
Functions Create Their Own Local Scope

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.

๐Ÿšซ
Blocks Don't Create Scope

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.

โœ๏ธ
Assignment Determines Locality

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.

๐ŸŒ
global Declares Module-Level Intent

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.

๐Ÿช†
nonlocal Declares Enclosing-Function Intent

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.

๐Ÿ”’
Reading Doesn't Require Declaration

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.

๐Ÿงฉ
Closures Capture Enclosing Variables by Reference

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.

๐Ÿ“›
Every Module Has Its Own Global Scope

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.

๐Ÿ—๏ธ
Built-in Scope Holds Python's Own Names

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.

โš ๏ธ
Mutating vs Reassigning Are Scope-Different Operations

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.

๐Ÿ” Name Referencede.g. print(total)
checks first
๐Ÿ“ L โ€” Local ScopeCurrent function's own namespace
found here
๐Ÿช† E โ€” Enclosing ScopeAny outer function this is nested in
found here
๐ŸŒ G โ€” Global ScopeCurrent module's top-level namespace
found here
๐Ÿ—๏ธ B โ€” Built-in ScopePython's own reserved names
found here
๐Ÿ’ฅ NameError Raisedname is not defined
โœ… Name FoundValue used at this line

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.

ScopeWhere It LivesLifetimeModifiable From Inner Function?Typical Use
Local (L)Inside the current function callCreated on call, destroyed on returnN/A โ€” it's already the innermost scopeFunction's own working variables
Enclosing (E)An outer function this one is nested insideTied to the outer function's call/closure lifetimeโœ… Yes, with nonlocalClosures, decorator state, factory functions
Global (G)Top level of the current module (.py file)Exists for the life of the program/moduleโœ… Yes, with globalModule-wide constants, shared application state
Built-in (B)Python's own reserved namespaceExists for the life of the Python processโŒ Cannot be reassigned this way (would shadow, not modify)len(), print(), range(), Exception, etc.

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.

FeaturePythonJavaJavaScript (let/const)C++
Scoping unitFunction scope (and module scope)Block scopeBlock scope (with let/const)Block scope
if/for block creates new scope?โŒ Noโœ… Yesโœ… Yes (with let/const)โœ… Yes
Variable declared inside a loop survives after it?โœ… Yes, within the same functionโŒ No, scoped to the loop blockโŒ No, scoped to the loop block (with let)โŒ No, scoped to the loop block
Explicit keyword to modify outer scope?โœ… global / nonlocalโŒ Not applicable (fields/access modifiers instead)โŒ Not applicable (closures just work)โŒ Not applicable (references/pointers instead)
Nested function closuresโœ… Yes, since Python 2.1 (2001)โš ๏ธ Only effectively-final variables (lambdas)โœ… Yes, fullyโš ๏ธ Explicit capture list required for lambdas
Default assignment behaviour inside a functionLocal, unless declared global/nonlocalLocal to the declared block automaticallyLocal to the declared block automatically (let/const)Local to the declared block automatically

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.

โœ… Advantages
Simple, Predictable, Consistent RuleLEGB is one deterministic search order, applied identically everywhere in the language โ€” no special cases for loops, conditionals, or try blocks to memorise separately.
No Unnecessary Block-Level CeremonyNot needing block scope means you don't need to declare variables above a loop just to use their final value afterward โ€” genuinely less boilerplate than block-scoped languages often require.
Genuine, Fully-Working ClosuresEnclosing scope access, combined with nonlocal, gives Python real closures capable of both reading and modifying captured state โ€” the foundation for decorators and factory functions.
Explicit Intent for Outer-Scope ModificationRequiring global and nonlocal for reassignment (though not for reading) forces a small, deliberate signal in the code whenever a function reaches outside its own local scope to change something.
Encapsulation by DefaultSince every function gets its own private local namespace automatically, variables genuinely don't leak between unrelated functions unless you explicitly ask them to via global or nonlocal.
Module-Level Global Scope Is Cleanly IsolatedBecause global scope is per-module rather than truly program-wide, importing modules doesn't silently pollute a shared global namespace the way some languages' global scope can.
โŒ Disadvantages
The Local-by-Default Assignment Rule Surprises BeginnersThat a single assignment anywhere in a function retroactively makes that name local for the WHOLE function, even before the assignment line, is genuinely counter-intuitive the first time it produces UnboundLocalError.
Silent Shadowing of Built-insNaming a local variable list, dict, or str silently hides Python's built-in of that name within that scope, with no warning โ€” a subtle bug source, especially for beginners unaware they've done it.
No Block Scope Can Cause Loop-Variable LeakageA variable assigned inside a for loop or if block remains accessible after that block ends, which occasionally causes confusion when a stale value from a previous iteration gets used unintentionally.
Two Separate Keywords for a Similar ConceptNeeding both global and nonlocal, rather than one unified keyword, adds a bit of real cognitive overhead โ€” remembering which one applies to which situation is a genuine, if small, learning cost.
Late-Binding Closures Can SurpriseBecause closures capture enclosing variables by reference, not by value at creation time, functions created in a loop that reference the loop variable often behave unexpectedly, all sharing the same final value.
Debugging Scope Issues Requires Understanding an Implicit RuleThere's no visible syntax marking a variable as local versus outer-scope at the point of assignment โ€” you have to know the local-by-default rule exists to correctly predict a function's behaviour just by reading it.

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.

Local Scope (L)
Innermost function's own namespaceParameters and locally assigned variablesCreated fresh on every call, destroyed on return
Enclosing Scope (E)
Any outer function this one is nested insideAccessed read-only by defaultModifiable with nonlocal
Global Scope (G)
Top level of the current .py moduleModule-level variables, imports, top-level function/class definitionsModifiable with global
Built-in Scope (B)
len, print, range, Exception, etc.Lives in the builtins moduleChecked last, can be silently shadowed
CPython Name Resolution Engine
Searches L โ†’ E โ†’ G โ†’ B in strict orderStops at first matchRaises NameError if exhausted
Result
Correct value bound to that nameUsed at the point of reference

Architecture Diagram

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.

๐Ÿ Pythonbroken_scope.py
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 value

The fix, using global:

๐Ÿ Pythonfixed_with_global.py
count = 0

def increment():
    global count
    count += 1
    print(count)

increment()
increment()
increment()

Output

1 2 3

And the closure version, using nonlocal instead โ€” a self-contained counter with no module-level global involved at all:

๐Ÿ Pythonclosure_counter.py
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_a

Output

1 2 1

Practice This Code โ€” Live Editor

Line-by-Line Explanation

  • โ–ถ

    count += 1 without global โ€” Because count is assigned inside increment(), Python treats it as local for the whole function, and reading it on the right side of += before it has a local value raises UnboundLocalError.

  • โ–ถ

    global count โ€” Declares, before any use, that count inside this function refers to the module-level variable. Now count += 1 correctly reads and writes that same global variable.

  • โ–ถ

    count = 0 inside make_counter() โ€” This is increment()'s enclosing scope variable, not global โ€” it belongs to make_counter()'s own local namespace.

  • โ–ถ

    nonlocal count inside increment() โ€” Declares that this inner function's count refers to the enclosing function's count, not a new local one and not the module-level global (there isn't one here anyway).

  • โ–ถ

    counter_a and counter_b โ€” Two separate calls to make_counter() create two entirely independent enclosing scopes, each with its own private count โ€” which is exactly why calling counter_b() starts back at 1, unaffected by counter_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.

ConceptWhat It IsExample / Tool
NamespaceA concrete mapping of names to objectsA function's local variables, accessible via locals()
ScopeThe rule for which namespaces are visible from whereThe LEGB search order itself
Local namespaceCurrent function call's own variableslocals() inside a function
Global namespaceCurrent module's top-level variablesglobals() anywhere in that module
Built-in namespacePython's reserved namesThe builtins module itself

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()

Easy

2. What happens when you run: x = 'global' \n def show(): print(x); x = 'local' \n show()?

Medium

3. Trace this: for i in range(3): pass \n print(i) โ€” what does it print, and why doesn't this raise an error?

Easy

4. What is the output of: def outer(): x = 'outer' \n def inner(): x = 'inner'; print(x) \n inner() \n print(x) \n outer()

Medium

5. What would change in the previous question if nonlocal x were added as the first line inside inner()?

Hard

6. What is the output of: list = [1, 2, 3] \n print(list(range(3)))

Hard

7. 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?

Medium

8. Why does globals()['x'] = 'changed' work to modify a global variable x from inside a function, even without the global keyword?

Hard

Conclusion โ€” 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.

Your SituationWhat to Focus On
Learning scope for the first timeโœ… LEGB search order โ€” memorise it, then trace it through real code
Function needs to modify a module-level variableโœ… global โ€” declared explicitly, before the variable is used
Nested function needs to modify an enclosing function's variableโœ… nonlocal โ€” for closures and factory-function patterns
Just reading an outer-scope variableโœ… No keyword needed at all โ€” LEGB handles it automatically
Getting UnboundLocalError unexpectedlyโœ… Check for ANY assignment to that name anywhere in the function
Heavy reliance on global mutable stateโš ๏ธ Consider passing values as explicit function arguments instead
Writing a decorator or factory functionโœ… Solid enclosing-scope and closure understanding is a genuine prerequisite
Debugging unfamiliar nested-function codeโœ… Use locals() and globals() to inspect exactly what's visible where

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. ๐Ÿ

Frequently Asked Questions (FAQ)