Function Namespaces and Scope
After completing this topic
You will be able to explain how Python searches for variable names (the LEGB rule) and handle situations where local and global variables conflict correctly.
Same Name, Different Value
x = 10
def foo(): x = 20 print(x)
foo() # 20print(x) # 10The x inside the function and the x outside are different variables, even though they have the same name. To understand this, you need to know about namespaces and scope.
What is a Namespace?
A namespace is a space that holds mappings between names and objects. In Python, it acts like a dictionary in the form of {name: object}.
x = 10name = "μ² μ"# Module namespace: {"x": 10, "name": "μ² μ", ...}def greet(): msg = "μλ
" # Function namespace: {"msg": "μλ
"}When a function is called, a new namespace is created, and it disappears when the function ends. Therefore, variables created inside a function cannot be used outside of it.
Scope and the LEGB Rule
Scope is the region of a code where a name is visible. When searching for a variable, Python explores in the LEGB order:
L β Local Inside the function
E β Enclosing Enclosing function (nested function)
G β Global Top level of the module
B β Built-in Python built-in (print, len, ...)x = "global" # G
def outer(): x = "enclosing" # E def inner(): x = "local" # L print(x) inner()
outer() # "local" β Because it was found in L, it stops searchingIt searches from the innermost (L) and goes outward if it's not found. It stops as soon as it finds it.
Local β Variables Inside a Function
def calculate(): result = 42 # Local variable temp = result * 2 return temp
calculate()print(result) # NameError: name 'result' is not definedVariables assigned (=) inside a function are automatically Local variables. They do not exist outside the function.
Global β Variables at the Top Level of a Module
counter = 0 # Global
def increment(): print(counter) # β
Reading is possible (found in G by LEGB) def reset(): counter = 0 # β οΈ A new Local variable is created! Independent of the Global counter
reset()print(counter) # Still 0 (Global is not changed)It is free to read Global variables inside a function. However, assigning to them creates a new Local variable, independent of the Global variable.
The global Keyword
To modify a Global variable inside a function, use the global keyword:
counter = 0
def increment(): global counter counter += 1
increment()increment()print(counter) # 2global counter declares that counter inside this function refers to the Global variable, not a Local one.
However, abusing global makes it difficult to track where values are changing. It is best to pass data using function parameters and return values whenever possible.
Enclosing β Nested Functions and nonlocal
def outer(): count = 0 def inner(): nonlocal count count += 1 return count print(inner()) # 1 print(inner()) # 2
outer()nonlocal declares that a variable is not Local, but belongs to the immediately enclosing function. It is used when creating closures.
Common Mistake β UnboundLocalError
x = 10
def broken(): print(x) # β UnboundLocalError! x = 20
broken()An error occurs. Python analyzes the entire function in advance, and if it sees x = 20, it decides that x is a Local variable. However, when print(x) is called, the Local x has not yet been assigned a value, so an error occurs.
Solution:
def fixed_1(): x = 20 # Assign first print(x)
def fixed_2(): global x # Explicitly declare that it is Global print(x) x = 20Built-in β Python Built-in Names
print(len([1, 2, 3])) # 3 β len is Built-in
# If you accidentally overwrite a built-in namelist = [1, 2, 3] # β οΈ Overwrites the Built-in listnew_list = list("abc") # TypeError!Names like list, dict, print, len are in the Built-in namespace. If you use these names as variables, you will overwrite the Built-in, and you will no longer be able to use the original functionality.
# Restoredel list # Deleting the Local/Global list makes the Built-in visible againClosures β Practical Use of Enclosing
def make_multiplier(factor): def multiply(x): return x * factor return multiply
double = make_multiplier(2)triple = make_multiplier(3)
print(double(5)) # 10print(triple(5)) # 15The multiply function remembers factor even after it is returned. This is a closure β a function that "closes" and carries variables from the scope in which it was defined.
def make_counter(): count = 0 def increment(): nonlocal count count += 1 return count return increment
counter = make_counter()print(counter()) # 1print(counter()) # 2print(counter()) # 3You can create functions that maintain state without creating classes.
Debugging Namespaces
def debug_scope(): x = 10 y = 20 print(locals()) # {'x': 10, 'y': 20}
debug_scope()print(globals().keys()) # All names in the global namespacelocals() and globals() return the namespace of the current scope as a dictionary. They are useful when you want to check where a variable belongs.
import builtinsprint(dir(builtins)) # All names in the Built-in namespaceKey Takeaways
| Keyword | Meaning |
|---|---|
| (None) | Assignment β Local, reference β Search in LEGB order |
global | "This variable belongs to the Global" |
nonlocal | "This variable belongs to the Enclosing function" |
Search order: Local β Enclosing β Global β Built-in
Assignment rule: If you use = inside a function, it is treated as LocalModules and Namespaces
import mathfrom os import path
# import separates namespaces by module nameprint(math.pi) # pi in the math namespaceprint(path.exists) # exists in os.path
# from import * pollutes the namespacefrom math import * # β οΈ All names are dumped into the current Globalprint(pi) # Works, but it is unclear where it comes fromimport math adds only the name math to the Global. from math import * dumps dozens of names into the Global, which increases the risk of name collisions.
Decorators and Scope
def log_calls(func): count = 0 def wrapper(*args, **kwargs): nonlocal count count += 1 print(f"[{count}th call] {func.__name__}") return func(*args, **kwargs) return wrapper
@log_callsdef greet(name): return f"Hello, {name}"
greet("μ² μ") # [1st call] greetgreet("μν¬") # [2nd call] greetDecorators are a practical use of closures. count lives in the Local (which is the Enclosing of wrapper) and is incremented with nonlocal with each call.
Namespace conflicts can cause bugs that are difficult to debug. By always being aware of "which scope this variable belongs to," you can avoid these mistakes.