Scope and Namespaces (The LEGB Rule)
In the previous lesson, we learned how to package code into reusable blocks using functions. But as soon as you start writing functions, questions immediately come up:
- Why can't I access a variable outside the function where it was created?
- Why does changing a variable inside a function sometimes leave the original variable completely untouched?
- Why did my code suddenly crash with
UnboundLocalErrorwhen I tried to update a counter? - Why does Python break if I name a list variable
list = [1, 2, 3]?
All of these behaviors are governed by two fundamental concepts in Python: Namespaces and Scope.
Understanding how Python tracks names and where it looks for them is the difference between guessing why your code works and writing predictable, clean, and bug-free programs.
Lesson Overview
By the end of this lesson, you will:
- Understand what a namespace is and how Python uses it to map names to objects.
- Distinguish between a namespace (where names live) and scope (where names are accessible).
- Know the lifetimes of different namespaces during program execution.
- Trace name resolution step-by-step using the LEGB rule (Local, Enclosing, Global, Built-in).
- Understand how variable reassignment creates local variables by default.
- Use the
globalandnonlocalkeywords correctly while understanding why relying on them is usually discouraged. - Recognize and avoid common pitfalls like variable shadowing and
UnboundLocalError.
Conceptual Overview
Namespaces: The Dictionaries of Python
A namespace is simply a container that maps names (identifiers) to objects.
Think of a namespace like the contact list in your phone:
- The "name" is the contact entry (e.g.,
"Mom"or"Alex"). - The "object" is the actual phone number and person behind it.
If two different people have a contact named "Alex" on their separate phones, there is no confusion because each "Alex" lives inside a completely separate contact list.
In Python, namespaces work the exact same way. In fact, most namespaces in Python are implemented under the hood as standard Python dictionaries, where the keys are the variable names (strings) and the values are the objects themselves!
This idea is so central to Python's architecture that Tim Peters included it as the final line of The Zen of Python:
"Namespaces are one honking great idea — let's do more of those!"
Scope: The Visibility Zone
While a namespace is the physical container (the dictionary mapping names to objects), scope is the textual region of a program where a namespace is directly accessible without needing special prefixes.
In other words:
- Namespace: "Where does this name live?"
- Scope: "From where in my code can I see and use this name?"
The Four Namespaces and Their Lifetimes
A Python program can have up to four types of namespaces active at various times. Each namespace has a distinct lifetime—meaning Python creates it when needed and destroys it when it's done:
| Namespace | When It Is Created | When It Is Destroyed | Example Contents |
|---|---|---|---|
| Built-in | When Python starts up. | When Python exits. | print(), len(), range(), int, str |
| Global | When the main script starts or a module is imported. | When the interpreter terminates. | Variables and functions defined at top level |
| Enclosing | When an outer function containing nested functions is called. | When that outer function finishes executing. | Variables in an outer function accessible to inner functions |
| Local | When a function is called. | When that function returns or finishes. | Parameters and variables created inside the function |
Because a function's local namespace is created freshly every time the function is called and deleted as soon as it returns, variables defined inside a function do not survive after the function finishes!
The LEGB Rule: How Python Finds Names
When you reference a variable name in your code, Python doesn't search randomly. It follows a strict, inside-out lookup order known as the LEGB rule:
- L — Local: Names assigned inside the current function or lambda (including its parameters).
- E — Enclosing: Names in the local scope of any enclosing (outer) functions, searched from the nearest enclosing function outward.
- G — Global: Names assigned at the top level of the current module or script.
- B — Built-in: Names pre-loaded by Python's built-in module (such as
print,len,sum,Exception).
If Python searches through all four levels and still cannot find the name, it stops and raises a NameError.
The step-by-step decision flow when resolving any variable:
Let's see the LEGB rule in action step-by-step.
1. Local vs. Global
When a variable exists in both the global and local scopes, the local variable takes precedence inside that function:
Notice what happened:
- Inside
show_value(), Python checked Local first. It foundx = "local x"and printed it. - Outside the function, only the Global scope exists, so
xis still"global x". The assignment insideshow_value()created a brand new local variable without touching the global one.
2. Enclosing Scope (Nested Functions)
In Python, you can define a function inside another function. The outer function creates an Enclosing scope for the inner function:
Here, when inner() runs:
- Python checks Local (inside
inner()). Nogreetingis defined there. - Python steps out to Enclosing (inside
outer()). It findsgreeting = "Hello from Enclosing"! - The search stops immediately. The global greeting is never checked.
3. Built-in Scope
If a name isn't found in Local, Enclosing, or Global, Python finally checks the Built-in namespace:
Modifying Variables Across Scopes
The Reassignment Rule: Reading vs. Writing
In Python, there is a fundamental difference between reading a variable from an outer scope and assigning to it:
- You can read global or enclosing variables inside a function without any special keywords.
- But as soon as you use the assignment operator (
=) on a name inside a function, Python treats that name as local to that function!
In-Place Modification of Mutable Objects
There is one important nuance: if an outer variable points to a mutable object (such as a list or a dict), you can mutate its contents from inside a function using methods or item assignment:
Because cart.append() is modifying the existing list object rather than reassigning the name cart = ..., Python looks up cart globally and modifies it in place.
The global Statement
What if you genuinely need to rebind or modify a global variable from inside a function? You can declare the variable using the global keyword:
global is DiscouragedWhile global is a valid feature of Python, using it frequently is considered a bad practice.
Global variables make programs difficult to reason about, test, and debug, because any function anywhere in the file could unexpectedly alter the variable's value.
Instead of modifying global variables, prefer passing data into functions as arguments and returning the updated value with return:
# Better: pure data flow
def increment(count):
return count + 1
counter = 0
counter = increment(counter)
The nonlocal Statement
When working with nested functions, if an inner function needs to modify a variable in its enclosing function, global won't work (because the variable isn't global). For this, Python provides the nonlocal keyword:
Without nonlocal count, the line count = count + 1 would cause Python to treat count as a local variable inside step(), resulting in an error.
Inspecting Namespaces: globals() and locals()
Python makes it easy to peek inside these namespace dictionaries using two built-in inspection functions:
globals(): Returns a dictionary representing the current global namespace.locals(): Returns a dictionary representing the current local namespace.
Common Beginner Traps
1. Shadowing Built-in Names
Because Python searches Local and Global scopes before the Built-in scope, you can accidentally "shadow" (mask) a built-in function by creating a variable with the same name:
# DO NOT DO THIS:
list = [1, 2, 3] # Overrides Python's built-in list()
# Now calling list() fails:
new_list = list(range(5))
# TypeError: 'list' object is not callable
If you accidentally shadowed a built-in in an interactive REPL session, you can remove your custom variable using the del statement:
del list # Removes your local/global variable, revealing the built-in again!
However, the best practice is to avoid using names like list, str, dict, sum, min, max, or id for your variables.
2. The UnboundLocalError Trap
Look at this common bug:
points = 50
def add_bonus():
print("Current points:", points)
points = points + 10 # Assignment makes 'points' local throughout the entire function!
add_bonus()
If you run this code, Python raises:
UnboundLocalError: cannot access local variable 'points' where it is not associated with a value
Why does this happen?
When Python compiles the body of add_bonus(), it sees points = ... and marks points as a local variable for the entire function. Then, when Python executes print(points) on line 4, it looks in the local scope, finds that the local variable points hasn't been assigned a value yet, and crashes!
To fix this:
- Either declare
global points(if you must rebind the global variable). - Or better yet: pass
pointsas an argument and return the new total:def add_bonus(current_points):
return current_points + 10
Deepen your Knowledge
Learn More: Official Documentation & Language Reference
Read through the following sections to strengthen your mental model of namespaces and scoping rules:
- Read Python Tutorial: Scopes and Namespaces Example (Section 9.2). Pay close attention to the code example comparing
do_local,do_nonlocal, anddo_global. - Read Python Execution Model: Naming and Binding (Section 4.2.1 and 4.2.2). Focus on how binding names associates them with specific scopes and why
UnboundLocalErroroccurs. - Review The Zen of Python (PEP 20) in your terminal by opening Python and typing
import this. - ReadNamespaces and Scope in Python
Check Knowledge
Test your understanding with the following questions. Some questions will require research using the links above:
- What does the acronym LEGB stand for, and in what order does Python search these scopes?
- Why does calling
globals()return a live reference that can modify the global namespace, whilelocals()inside a function returns a copy? - If an inner function references a variable that exists in both an enclosing function and the global scope, which value will it access?
- What happens if you assign a value to a variable inside a function without using
globalornonlocal? Does it affect an outer variable of the same name? - Why does running the code below produce an error, and which specific exception is raised?
total = 100
def adjust():
total += 5
adjust() - What is the difference between the
globalstatement and thenonlocalstatement? Cannonlocalbe used to refer to a module-level global variable?
Exercise
Visit our python-exercises repository to update your local copy and test your understanding of variable scopes:
- Fetch and pull the latest changes from
upstream/main. - Locate the exercise directory
exercises/foundations/06_scopes. - Run
pytestto inspect the test suite. - Implement the required functions and variable scopes level-by-level, removing the
@pytest.mark.skipdecorator as you progress.
Assignment
Return to your simple-bookstore project from previous lessons. In this assignment, you will audit and refactor how variables and state are organized across your bookstore script.
You will complete this assignment on your local machine:
- Open your
simple-bookstoredirectory in your code editor. - Open
main.pyand inspect your variables:- Identify your configuration settings (such as
STORE_NAME,TAX_RATE,PIN_CODE,UNLOCK_ATTEMPTS). Ensure these are defined as module-level constants at the top of your file usingUPPER_SNAKE_CASE. - Verify that your functions (like
calculate_subtotal,calculate_tax,format_receipt) do not use theglobalkeyword to read or modify calculation totals. - Refactor any function that directly modifies external variables so that it receives needed values via parameters and returns new values via
return.
- Identify your configuration settings (such as
- Create a helper function
apply_member_discount(subtotal, is_member):- Define a local constant or variable
DISCOUNT_RATE = 0.10inside the function. - If
is_memberisTrue, calculate and return the discounted subtotal. Otherwise, return the originalsubtotal. - Verify that attempting to print
DISCOUNT_RATEoutsideapply_member_discount()raises aNameError, confirming it is safely scoped locally.
- Define a local constant or variable
- Check your code for variable shadowing: ensure you have not named any variable
list,str,sum, ormin. - Run your bookstore application with
python main.pyand ensure the unlock system, shopping calculations, and receipt formatting work seamlessly. - Stage, commit with a descriptive message (e.g.,
git commit -m "Refactor bookstore variables and enforce clean scoping"), and push your changes to your remote GitHub fork.
What's Next
Now that we understand where variables live and how Python resolves names, our functions are well-structured and safe from unintended side effects.
However, as programs grow larger, how do we—and our code editors—know what types of arguments a function expects and what type of data it returns?
In the next lesson, we will explore Type Hints to make our functions self-documenting, safer, and easier to maintain!