Scope and Namespaces¶
Understanding scope is one of those things that separates programmers who "know Python" from programmers who understand Python. It explains why a variable defined in a function isn't visible outside it, and why modifying a global inside a function requires the global keyword.
What Is a Namespace?¶
A namespace is a dictionary mapping names to objects. When you write x = 42, Python stores the name "x" in the current namespace, pointing at the integer object 42.
Python maintains several namespaces at any point:
- Built-in:
print,len,int,range,Exception, etc. — always available - Global: The module-level namespace — one per
.pyfile - Local: Created fresh for each function call — destroyed when the function returns
- Enclosing: Any intermediate function scopes (for closures)
# Global namespace
x = 10 # 'x' lives in the module's global namespace
def outer():
y = 20 # 'y' lives in outer()'s local namespace
def inner():
z = 30 # 'z' lives in inner()'s local namespace
The LEGB Rule¶
When Python looks up a name, it searches scopes in this exact order:
- Local — the current function
- Enclosing — any enclosing functions (inner to outer)
- Global — the module level
- Built-in — Python's built-in names
The first match wins. If not found in any scope, you get a NameError.
x = "global" # G — global scope
def outer():
x = "enclosing" # E — enclosing scope
def inner():
x = "local" # L — local scope
print(x) # finds 'x' in L → "local"
inner()
print(x) # finds 'x' in E → "enclosing"
outer()
print(x) # finds 'x' in G → "global"
Built-in Lookup¶
# 'len' is in the built-in scope — always accessible
print(len([1, 2, 3])) # 3
# You can shadow built-ins (but don't!)
len = 100 # shadows the built-in len
print(len([1, 2, 3])) # TypeError: 'int' object is not callable
del len # restore access to the built-in
print(len([1, 2, 3])) # 3
Local Scope — Functions¶
Every function call creates a fresh local namespace. Variables defined inside a function are local to it:
def calculate():
result = 42 # local to calculate()
return result
calculate()
# print(result) # NameError! 'result' doesn't exist here
Parameters are also local:
def greet(name): # 'name' is local
message = f"Hello, {name}"
return message
greet("Alice")
# print(name) # NameError
Global Scope¶
Code at the top level of a module lives in the global namespace. Functions can read global variables without any special keyword:
threshold = 100 # global
def is_over_threshold(value):
return value > threshold # reads the global 'threshold' — fine
But assigning to a name inside a function creates a new local — it does NOT modify the global:
count = 0
def increment():
count = count + 1 # UnboundLocalError! Python sees 'count =' and
# decides 'count' is local — but then tries to read
# it before it's assigned
increment() # UnboundLocalError: local variable 'count' referenced before assignment
This surprises many people. Python decides at compile time whether a name is local (if it appears on the left side of = anywhere in the function), so the read count + 1 tries to read the local count which hasn't been assigned yet.
The global Statement¶
Use global to tell Python that a name refers to the global scope:
count = 0
def increment():
global count # 'count' now refers to the module-level 'count'
count = count + 1
increment()
increment()
increment()
print(count) # 3
When to use global
Use it sparingly. Mutable global state makes code hard to test and reason about. Prefer returning values or using classes to encapsulate state.
Enclosing Scope and Closures¶
When a function is defined inside another function, it has access to the enclosing function's local variables — even after the outer function has returned. This is called a closure:
def make_multiplier(factor):
# 'factor' is in the enclosing scope of the inner function
def multiply(x):
return x * factor # 'factor' captured from enclosing scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# 'factor' lives on because it's referenced by the closure
The nonlocal Statement¶
A closure can read enclosing variables freely, but assigning to them creates a new local (same issue as global). Use nonlocal to modify an enclosing variable:
def make_counter():
count = 0
def increment():
nonlocal count # 'count' refers to the enclosing 'count'
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
nonlocal searches through enclosing scopes (not global, not built-in) for the name. It's an error if the name isn't found in any enclosing scope.
Class Scope — A Special Case¶
Class bodies have their own namespace during execution, but methods don't see the class scope via normal LEGB lookup:
class Config:
DEBUG = True
MAX_RETRIES = 3
def is_debug(self):
# return DEBUG # NameError! Not in local or global scope
return Config.DEBUG # Correct — access via class name
# return self.DEBUG # Also correct — via instance
Config().is_debug() # True
This surprises everyone from Java/C++ backgrounds where DEBUG would be visible inside a method.
Comprehension Scope¶
List, dict, and set comprehensions have their own scope in Python 3. The iteration variable doesn't leak:
x = "outer"
result = [x for x in range(5)] # 'x' inside comprehension is local to it
print(x) # "outer" — unchanged! (In Python 2 this would be 4)
print(result) # [0, 1, 2, 3, 4]
Generator expressions also have their own scope.
locals() and globals()¶
These built-in functions return the current local and global namespaces as dictionaries:
x = 10
y = 20
def show_namespaces():
a = 1
b = 2
print("Locals:", locals()) # {'a': 1, 'b': 2}
print("Globals:", list(globals().keys())[:5]) # first few global names
show_namespaces()
locals() returns a copy
Modifying the dict returned by locals() does not change the actual local variables (in most cases). globals() returns a live reference — modifying it does affect globals, but avoid doing so in production code.