Historical Context & Motivation
Imagine you and your friend both have a dog named Max. At your house, "Max" means your golden retriever. At your friend's house, "Max" means their terrier. Neither household gets confused because each home is a separate scope — a boundary that determines which Max you're talking about. Programming languages face the exact same problem: when two variables share a name, the computer needs rules to decide which one you mean.
Early programming languages in the 1950s and 1960s had very simple approaches to this problem. In some early versions of BASIC, every variable was global — visible everywhere in the program. This meant that if you used a variable called x in one part of your code, it could accidentally be changed by a completely different part. As programs grew larger, this became a nightmare for debugging.
nonlocal keyword, finally letting inner functions modify variables in an enclosing function's scope without resorting to workarounds.The central question scope answers is deceptively simple: when your code mentions a variable name, where does Python look for it, and what happens if the same name exists in multiple places? Understanding this question is essential to writing functions that behave predictably and don't produce mysterious bugs.
Core Principles & Definitions
In Python, scope refers to the region of your program where a particular variable name is accessible. Think of scope as a set of boundaries — like walls in a building — that determine which parts of your code can "see" a variable. Python organizes these boundaries into a clear hierarchy, and understanding the hierarchy is the key to mastering scope.
Local Scope
Global Scope
global keyword.Enclosing (Nonlocal) Scope
nonlocal keyword lets you modify those variables.Built-in Scope
print, len, and range live in the built-in scope. This is the outermost layer — Python checks here last.Visual Explanation — The LEGB Rule
In the diagram above, notice that each scope is physically contained within the next larger scope. The local scope (innermost cyan box) is where Python always begins its search. If the variable isn't found there, Python moves outward to the enclosing scope (pink box), then to the global scope (violet box), and finally to the built-in scope (amber box). If the name isn't found in any of these four layers, Python raises a NameError.
How Scope Works in Practice
Reading vs. Writing — The Critical Difference
One of the trickiest parts of scope is the difference between reading a variable and writing to a variable. Python lets you read a global variable from inside a function without any special syntax. However, if you try to assign a value to that name inside the function, Python assumes you're creating a new local variable with the same name — it does not modify the global one.
The global Keyword
To explicitly tell Python "I want to modify the global variable, not create a local one," you use the global keyword at the top of your function. This declaration forces Python to skip creating a local variable and instead work directly with the global one. Here's the pattern:
score = 0
def add_point():
global score ← tells Python to use the global score
score = score + 1
Without the global declaration, the assignment score = score + 1 would cause an UnboundLocalError because Python sees the assignment and treats score as local, but then tries to read it before it has a value.The nonlocal Keyword
The nonlocal keyword solves the same problem as global, but for nested functions. When you have a function defined inside another function, nonlocal tells the inner function to modify the variable in its enclosing function's scope rather than creating a new local variable. Without nonlocal, the inner function can read the enclosing variable but cannot reassign it.
def outer():
count = 0
def inner():
nonlocal count ← modifies outer's count
count = count + 1
inner()
print(count) # prints 1global or nonlocal. This prevents accidental changes to variables in other scopes — like having a lock on each room's door so visitors can look through the window (read) but can't rearrange the furniture (write) without a key.LEGB Lookup — Step by Step
Let's trace exactly how Python resolves a variable name through the LEGB chain with a concrete code example. Understanding this flow is the single most important skill for debugging scope-related errors.
x when print(x) is called inside inner(). It checks each scope in LEGB order, stopping at the first match. The left panel shows how removing definitions changes which value is found.The flowchart illustrates three important scenarios. In Case A, all three x definitions exist, so Python finds x immediately in the local scope and prints "local". In Case B, we remove the local assignment, so Python skips L and finds x in the enclosing scope. In Case C, both local and enclosing are removed, so Python reaches the global scope. The key insight is that a local variable with the same name as a global variable "shadows" the global one — the global variable still exists, but it's hidden from view inside that function.
Worked Example — Tracing Scope
Let's carefully trace through a program that uses local, global, and nonlocal scope to understand exactly what happens at each step.
message = "hello"
def greet():
message = "hi"
def excited():
nonlocal message
message = message + "!!!"
excited()
print("Inside greet:", message)
greet()
print("Global:", message)message = "hello" at the top level. This creates a variable message in the global scope with the value "hello".message = "hello"greet() and enters the function. A new local scope is created for greet. The line message = "hi" creates a new local variable — it does NOT change the global message.message = "hi" | Global: message = "hello" (unchanged)excited(). The nonlocal message declaration tells Python: "When I say message in this function, I mean the one from the enclosing scope (greet's local variable)." So message = message + "!!!" reads "hi" from greet's scope and changes it to "hi!!!".message = "hi!!!" (modified by nonlocal)excited() returns, print("Inside greet:", message) executes inside greet. It finds message in the local scope with the updated value.Inside greet: hi!!!greet() returns, Python executes print("Global:", message) at the top level. The global message was never touched — neither greet nor excited used the global keyword.Global: hellononlocal keyword in excited() modified greet's local variable but left the global variable completely untouched. Each scope is like a separate notebook — nonlocal lets you write in your teacher's notebook, but it doesn't touch the principal's notebook (global).global vs. nonlocal — When to Use Each
Students often confuse global and nonlocal because they both solve the same basic problem: letting a function modify a variable from an outer scope. However, they target different levels of the scope hierarchy and have different use cases.
| Feature | global | nonlocal |
|---|---|---|
| Target scope | The module-level (top-level) scope | The nearest enclosing function's scope |
| Requires nesting? | No — works in any function | Yes — only in a function inside another function |
| Variable must exist? | No — can create a new global variable | Yes — the variable must already exist in an enclosing scope |
| Common use case | Configuration values, counters shared across many functions | Closures, counters in nested helper functions |
| Best practice | Use sparingly — overuse makes code hard to debug | Useful for closures; generally cleaner than global |
global and nonlocal are used cautiously. The cleanest approach is often to pass values as arguments and return results rather than modifying outer variables directly. Think of it like a well-run restaurant kitchen: instead of everyone reaching into the same pot (shared mutable state), each chef receives ingredients, prepares their dish, and passes the finished plate to the next station.Connection to Advanced Concepts
Understanding scope is a gateway to more powerful programming concepts. Once you're comfortable with how Python resolves names, you'll be ready to explore closures, decorators, and even object-oriented programming — all of which rely heavily on scope rules.
| What You Know Now | Where It Leads |
|---|---|
| Local variables are created when a function runs and destroyed when it ends | Closures — an inner function can "remember" variables from its enclosing scope even after the outer function has finished |
| Functions can be nested inside other functions | Decorators — functions that wrap other functions to add extra behavior, using nested scope extensively |
| The LEGB rule determines name resolution | Namespaces & modules — each imported module has its own global scope, preventing name collisions across files |
| Scope isolates variables to prevent accidental modification | Encapsulation in classes — object-oriented programming uses similar ideas to protect data inside objects |
As you continue learning Python, you'll find that nearly every design pattern and advanced technique builds on the foundation of scope. A closure, for instance, is essentially a function that takes advantage of the LEGB rule by "capturing" variables from its enclosing scope. Mastering scope now will make these future topics feel like natural extensions rather than brand-new concepts.
Practice Problems
x = 5
def change_x():
x = 10
print(x)
change_x()
print(x)total variable.
total = 0
def add_to_total(n):
total = total + n
add_to_total(5)
print(total)nonlocal makes it work.
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
my_counter = make_counter()
print(my_counter())
print(my_counter())
print(my_counter())inner() to modify the global variable score, but the output is 0. Explain the bug and propose two different fixes.
score = 0
def outer():
score = 0
def inner():
nonlocal score
score += 10
inner()
outer()
print(score)Summary — Scope: Local, Global & nonlocal
Python's scope system determines where a variable can be accessed in your code. Variables exist in one of four layers, searched in LEGB order: Local (inside the current function), Enclosing (an outer function's scope), Global (the top level of your script), and Built-in (Python's pre-loaded names like print and len). Assignments inside a function create local variables by default.
To modify a global variable from inside a function, use the global keyword. To modify a variable in an enclosing function's scope, use the nonlocal keyword. Without these keywords, Python protects outer variables from accidental changes — a design choice called variable shadowing. When a local variable shares a name with a global one, the local version hides (but does not delete) the global. Understanding scope is essential for writing clean, bug-free functions and is the foundation for advanced topics like closures and decorators.