Python Scope and Namespaces
Master Python scope and namespaces: how variable lookup works in LEGB rule, practical examples, and common pitfalls. Hands-on lesson for Python developers.
Focus: python scope and namespaces
Have you ever written a function, only to be baffled when a variable inside it doesn't behave as you expected? Or tried to debug a frustrating "UnboundLocalError" or a simple name error that seems to appear out of nowhere? These are the classic symptoms of not understanding Python's scope and namespaces — the invisible rules that dictate where your variables live and how Python finds them. Mastering this concept is not just academic; it's the key to writing predictable, bug-free code and avoiding some of the most common and confusing errors in Python.
The problem this lesson solves
Without a clear mental model of variable scope, your Python code can behave in unpredictable ways. A function might unexpectedly overwrite a global variable, or you might get an error trying to access a variable you know exists. This lesson solves that confusion by giving you a concrete, step-by-step understanding of how Python manages variable names. Once you grasp the LEGB rule (Local, Enclosing, Global, Built-in), you'll be able to predict exactly how any name lookup will resolve. You'll stop writing code that works by accident and start writing code that works with intention.
Core concept / mental model
Think of Python's namespaces as a set of nested dictionaries, or like a system of filing cabinets. When your code tries to access a variable name, Python searches through these cabinets in a strict order. This search order is known as the LEGB rule:
- Local — Inside the current function or class.
- Enclosing — In functions that enclose the current function (e.g., outer function of a nested function).
- Global — At the top level of the module/script.
- Built-in — Pre-defined names in Python (like
print,len,True).
Mental model: Imagine you are looking for a specific word in a book. You first check your own desk (Local). Then the bookshelf next to you (Enclosing). Then the entire library (Global). Finally, you check the dictionary on the librarian's desk (Built-in). You stop as soon as you find the word, even if it exists in multiple places.
A namespace is a mapping from names to objects. Every time a function is called, a new local namespace is created. The global namespace is created when the script starts. The built-in namespace is always available. The key rule: assignment (the = operator) creates or updates names within the current scope, unless you explicitly use global or nonlocal to indicate otherwise.
How it works step by step
Let's walk through how Python resolves a variable name when your code runs.
- Start: Python encounters a name (e.g.,
x). - Search Local: Python first checks the innermost scope — the current function's local namespace. If
xis found there, the search stops immediately. - Search Enclosing: If not found locally, Python looks in the namespace of any enclosing functions (outer functions of nested definitions). It will look in the nearest enclosing scope first, then the next, and so on.
- Search Global: If still not found, Python searches the global (module-level) namespace.
- Search Built-in: If
xis not found in any of the above, Python finally checks the built-in namespace. If it's not there either, you get aNameError.
This process happens at runtime, not at compile time. This is why a variable defined after a function call can still be accessed inside that function if it's looked up later (as long as it's not assigned inside the function).
Hands-on walkthrough
Let's solidify this with examples. All examples assume Python 3.10+.
Example 1: Simple Local vs Global
x = "global"
def my_func():
x = "local"
print(f"Inside function: {x}")
my_func()
print(f"Outside function: {x}")
Expected output:
Inside function: local
Outside function: global
The x = "local" inside my_func creates a new variable in the local namespace, shadowing the global x. The global x is unchanged. This is a critical point: assignment inside a function always targets the local scope by default.
Example 2: Modifying a Global Variable (using global)
y = 10
def add_to_global(value):
global y # Tell Python we intend to modify the global 'y'
y += value
global_initial = y
add_to_global(5)
print(f"Global y after: {y}")
Expected output:
Global y after: 15
Without the global y declaration, Python would raise an UnboundLocalError because the assignment y += value would make Python think y is local, but it's being used before assignment on the right-hand side.
Example 3: Nested Functions and nonlocal
def outer():
x = "outer value"
def inner():
nonlocal x # Tell Python we want to modify 'x' from the enclosing scope
x = "inner modified"
print(f"Inner sees: {x}")
inner()
print(f"Outer sees after inner: {x}")
outer()
Expected output:
Inner sees: inner modified
Outer sees after inner: inner modified
- Without
nonlocal x, theinner()function would create a new localx, leavingouter()'sxunchanged. nonlocalis used to rebind a variable in the nearest enclosing scope that is not global. It's specifically for nested functions.
Example 4: Looking up a variable defined after the function
def show_z():
print(f"Value of z is: {z}")
z = 100
show_z()
Expected output:
Value of z is: 100
This works because the function only looks up z when it is called, not when it is defined. At the time of the call, z exists in the global namespace.
Compare options / when to choose what
| Approach | Use Case | Risk |
|---|---|---|
| Local variables (default inside a function) | Most common — data that should be private to a function | None; it's the safest default |
global keyword |
Rarely — sharing a configuration-like state across functions | Makes code harder to reason about, test, and debug; can cause unintended side effects |
nonlocal keyword |
Only inside nested functions when you need to modify an enclosing scope's variable | Can still be confusing for readers; prefer passing data as arguments/return values if possible |
| Arguments/return values | The preferred alternative to global and nonlocal — data flows explicitly |
Can be verbose in deeply nested code, but clarity wins |
Pro tip: If you find yourself reaching for
global, ask if you can instead pass the value as a parameter and return the result. It makes your functions pure and predictable.
Troubleshooting & edge cases
-
UnboundLocalError: You get this if you try to assign to a variable inside a function without declaring it
globalornonlocal, but also try to read the variable's value before the assignment. Python sees the assignment and decides the variable is local, so the read fails. Fix: Useglobalornonlocal, or rename the local variable. -
Accidentally shadowing built-in functions: If you name a local variable
listorprint, you'll replace the built-in function within that scope. Fix: Avoid using built-in names for your variables. -
Mutation vs. Rebinding: If you have a global list
my_list = [], callingmy_list.append(1)inside a function works withoutglobal, because you are mutating the object, not rebinding the name. Butmy_list = [1]inside the function would create a new local variable. Fix: Understand that assignment changes the name binding; method calls on objects do not. -
Class scope oddity: In a class definition, variable assignments create a class-local namespace, but nested functions (methods) do not see class-level variables unless accessed via
selfor__class__. Fix: Access class variables viaself.var(if you mean instance) orClassname.var.
What you learned & what's next
You now understand the core rules of Python's scope and namespaces. You can:
- Explain the LEGB rule and trace how Python resolves any variable name.
- Distinguish between local, global, enclosing, and built-in namespaces.
- Correctly use
globalandnonlocalwhen you actually need to modify a variable from an outer scope. - Debug common errors like
UnboundLocalErrorand accidental shadowing.
In the next lesson, you'll build on this knowledge by exploring closures and decorators — powerful patterns that rely heavily on understanding the enclosing scope and how functions retain references to their parent environments. You are now ready to write more robust, maintainable Python code.
Practice recap
Try this: create a nested function where the inner function modifies a variable from the outer function using nonlocal. Then, create a counter that increments every time you call an inner function. Finally, try to write a function that uses global to track how many times it has been called across the module. Run everything in a script to verify the behavior.
Common mistakes
- Forgetting that assignment creates a local variable: using
globalornonlocalis required to rebind a name in an outer scope. - Trying to use
nonlocalwhen the variable is in the global scope —nonlocalonly works for enclosing function scopes, not the module-level global scope. Useglobalinstead. - Assuming
globalis needed for mutable objects (like list append or dict update). Mutation does not change the binding, soglobalis unnecessary; it's only required for reassignment. - Shadowing built-in names unintentionally by naming a local variable
list,dict,print, etc., which can break other parts of your code until the scope exits.
Variations
- Use the
inspectmodule (inspect.currentframe().f_locals) to programmatically inspect namespaces for debugging purposes. - Leverage
exec()oreval()with a custom namespace dictionary (e.g.,exec(code, my_globals, my_locals)) to dynamically control variable scope at runtime. - Use closures (functions that capture enclosing scope) for a design pattern that replaces
globalusage, especially for creating stateful factories.
Real-world use cases
- Configuration management: loading settings from a config file into a global namespace, where functions can read but not accidentally overwrite them using read-only access via
__getattr__on a module. - Caching/memoization: a decorator storing results in an enclosing scope (closure) to cache expensive function calls without polluting the global namespace.
- Implementing a simple state machine by using
nonlocalin nested functions to track state transitions while keeping the state variable private to the outer function.
Key takeaways
- Python's LEGB rule defines how variable names are resolved: Local, Enclosing, Global, Built-in — search stops at the first match.
- Assignment inside a function always creates a name in the local scope by default; use
globalornonlocalto rebind in an outer scope. - Mutation of an object (e.g., list.append) does not require
globalbecause the name binding is unchanged; only rebinding does. nonlocalworks only for nested functions and modifies the nearest enclosing scope that is not global;globalworks at the module level.- Shadowing built-in names (e.g.,
list) can cause subtle bugs — always avoid naming your variables after built-in Python keywords and functions.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.