Execution Model¶
Python's execution model defines how code runs — how names are resolved, what a "scope" is, and why a variable defined inside a function isn't visible outside it.
Code Blocks¶
Python executes code in units called code blocks. A new code block is created by:
- A module (the entire file is one block)
- A function body (each function has its own block)
- A class body
- A comprehension (
[... for ...]) - A generator expression
exec()andeval()calls
Each code block has its own namespace — a dictionary mapping names to objects.
Namespaces¶
A namespace is a mapping from names to objects. Python has several:
- Built-in namespace:
print,len,int,Exception, etc. — always available - Global namespace: The module-level namespace — one per module
- Local namespace: Created fresh each time a function is called
# Global namespace
x = 10 # 'x' is in the module's global namespace
def my_func():
y = 20 # 'y' is in my_func's local namespace
print(x) # 'x' found in global namespace
print(y) # 'y' found in local namespace
The LEGB Scope Rule¶
When Python looks up a name, it searches in this order:
L → Local: current function's namespace
E → Enclosing: any enclosing functions (closures)
G → Global: the module-level namespace
B → Built-in: Python's built-in namespace
x = "global" # G
def outer():
x = "enclosing" # E
def inner():
x = "local" # L
print(x) # L → "local"
inner()
print(x) # E → "enclosing"
outer()
print(x) # G → "global"
The global Statement¶
By default, assignment inside a function creates a new local variable, even if a global with the same name exists. Use global to modify a global:
count = 0
def increment():
global count # 'count' refers to the global, not a new local
count += 1
increment()
increment()
print(count) # 2
Avoid global when you can
Overusing global makes code hard to follow. Prefer returning values or using classes to encapsulate state.
The nonlocal Statement¶
nonlocal lets an inner function modify a variable from an enclosing (but not global) function:
def make_counter():
count = 0 # enclosing variable
def increment():
nonlocal count # refers to the enclosing 'count'
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
This is the correct way to write closures that modify state.
Name Resolution in Class Bodies¶
Class bodies have their own scope during execution, but methods don't see the class scope via normal LEGB lookup:
class Foo:
x = 10
def method(self):
# print(x) # NameError! 'x' is not in local or global scope
print(self.x) # Correct: access via self
print(Foo.x) # Also correct: access via class name
This surprises many people coming from other languages.
Builtins Can Be Shadowed¶
Because B (Built-in) is last in LEGB, any local or global with the same name shadows the built-in:
# Don't do this:
list = [1, 2, 3] # shadows the built-in 'list' type
print(list([4, 5])) # TypeError: 'list' object is not callable
# Recovery:
del list # removes the shadowing name
print(list([4, 5])) # works again
Common accidental shadows: list, dict, set, type, id, input, print, max, min, sum, len.
How Names are Resolved in Comprehensions¶
Comprehensions (list, dict, set, generator) have their own scope since Python 3. The iteration variable doesn't leak:
x = 10
squares = [x**2 for x in range(5)] # 'x' in comprehension is local to it
print(x) # 10 — unchanged! (in Python 2 this would be 4)
This is a common gotcha for people coming from Python 2.
Mutable Default Arguments and Closures¶
The LEGB rule interacts with default argument evaluation. Default argument values are evaluated once at function definition time, not at call time:
import time
def log(message, timestamp=time.time()): # evaluated ONCE when defined
print(f"[{timestamp}] {message}")
# timestamp is the same every call
log("event 1") # [1694700000.0] event 1 (example)
log("event 2") # [1694700000.0] event 2 (same timestamp!)
For mutable defaults the problem is different — the object is shared across all calls. See the Functions page for the fix.
Frame Objects¶
Each function call creates a frame object — a runtime record containing:
- The function's local namespace
- A reference to the previous frame (call stack)
- The code object being executed
- The current instruction pointer
You can inspect the current frame:
import sys
def show_frame():
frame = sys._getframe()
print(f"Function: {frame.f_code.co_name}")
print(f"File: {frame.f_code.co_filename}")
print(f"Line: {frame.f_lineno}")
print(f"Locals: {frame.f_locals}")
show_frame()
This is used by debuggers, profilers, and tracing tools.