Python Execution Model
Python's execution model interprets and runs code line by line, using an interpreter and memory management to execute programs efficiently.
The Python execution model is the system by which Python code blocks execute within execution frames, create and use namespaces, establish name bindings and scopes, resolve names, evaluate annotation scopes, propagate exceptions, and participate in the conceptual Python runtime structure. This model governs how Python programs run by defining the relationships and behaviors among code blocks, frames, namespaces, bindings, scopes, interpreters, thread-specific runtime states, and exception handling, without conflating these distinct concepts.
Foundations of the Python Execution Model
Execution in Python is the runtime realization of previously valid Python code. This means that execution involves performing the operations defined by the code after it has passed lexical analysis (tokenization) and grammatical parsing (syntax tree construction). Execution differs from these earlier stages and from the object protocols defined by the data model, which specify how objects behave and interact at runtime.
The execution model comprises several related but distinct concepts:
- Code blocks are units of Python program text executed as a whole.
- Execution frames are runtime contexts that maintain information about the current execution state for a code block.
- Namespaces are mappings from names to objects used during execution.
- Bindings associate names with objects in namespaces.
- Scopes define visibility regions where bindings can be resolved directly.
- Name resolution is the process of locating the binding for a name during execution.
- Exceptions propagate control flow dynamically when errors or special conditions occur.
- Interpreters represent isolated environments executing Python bytecode or instructions.
- Thread-specific runtime state captures execution state isolated per thread.
These concepts interact but are not interchangeable. For example, a frame uses namespaces but is not itself a namespace; an interpreter can host multiple thread states, and scopes define visibility but are not themselves storage mappings.
Python execution involves both compile-time determination of certain structural properties—such as which names are local to a code block—and runtime operations, such as resolving free variables and performing object operations. Implementation details like bytecode format, frame layout, interpreter loops, optimizers, or memory representations are distinct and vary by Python implementation, but the language-level execution rules remain consistent.
| Concept | Execution-Model Responsibility |
|---|---|
| Code Block | Defines a unit of executable Python code text |
| Frame | Holds runtime context and manages execution state for a code block |
| Namespace | Maps names to objects within a given context |
| Binding | Associates a name with an object in a namespace |
| Scope | Defines where bindings are directly visible through name resolution |
| Environment | The collection of accessible scopes and namespaces at a point in execution |
| Interpreter | Maintains persistent interpreter-specific state and executes code |
| Thread State | Tracks thread-specific execution state and manages concurrency |
This figure conceptually depicts a Python code block executing within an execution frame, which uses local, global, and builtins namespaces to resolve names. The frame exists within a thread-specific runtime state, which in turn is part of an interpreter environment.
Python Code Blocks
A code block is a piece of Python program text executed as one unit. It is not simply an arbitrary indented region of source code but a syntactically and semantically meaningful unit that the Python execution model recognizes as having a distinct execution context.
Fundamental code blocks include:
- Modules: The entire source text of a module forms a code block.
- Function bodies: The statements inside a function definition constitute a code block.
- Class definitions: The body of a class definition is a code block.
Additional forms of code blocks include:
- Interactive commands entered in a Python shell.
- Script input files executed as programs.
- Command strings supplied with the
-coption. - Modules executed as top-level programs via
-m. - Source strings supplied to
evalorexec.
Nested syntactic suites such as the bodies of if, for, while, try, or with statements do not automatically form independent code blocks and do not establish separate local scopes merely because they are indented.
# Example illustrating code blocks
x = 10 # Module-level code block
class C:
y = 20 # Class code block
def method(self):
if x > 5:
z = x + self.y # Nested suite, not separate code block
print(z)
In this example:
- The entire module is one code block.
- The class body
Cis a separate code block. - The function body of
methodis a separate code block. - The
ifstatement body insidemethodis not a separate code block.
Executing a code block occurs within an execution frame, and the kind of code block influences the namespace it creates and the way name resolution behaves.
Python Execution Frames
An execution frame is the runtime context in which one code block executes. It maintains information needed to manage local variables, global variables, builtins, the current code object, and the continuation of execution.
Frame objects expose various attributes that provide introspection into the execution state:
| Attribute | Description |
|---|---|
f_code | The code object currently being executed |
f_locals | Dictionary mapping local variable names to objects |
f_globals | Dictionary mapping global names to objects |
f_builtins | Dictionary mapping built-in names to objects |
f_back | Reference to the previous (caller) frame object |
| Execution position | Information about the current instruction or line number being executed |
The chain of active execution frames reflects the dynamic call stack during program execution. This chain differs from the lexical nesting of source definitions; for example, a function can be called from many different contexts, each with its own frame.
Generators, coroutines, and other suspended execution forms preserve execution state across suspensions, allowing resumption without restarting from the beginning.
Example introspection of frame data:
import sys
def example_func():
frame = sys._getframe()
print("Code object:", frame.f_code)
print("Locals:", frame.f_locals)
print("Globals:", frame.f_globals)
print("Builtins keys sample:", list(frame.f_builtins.keys())[:5])
print("Caller frame:", frame.f_back)
example_func()
This code obtains the current frame inside example_func and prints information about the code object, local and global namespaces, built-ins, and the caller frame without modifying state.
Execution frames use namespaces but are not identical to them; a frame encapsulates runtime execution state beyond namespace mappings.
Python Namespace Model
A namespace is a mapping from names (identifiers) to objects. The namespace relationship associates names with objects but does not imply ownership or copying of the objects.
Distinct namespaces participate in execution:
- Module global namespaces: Created per module; the module run as a program is named
__main__. - Function local namespaces: Created when a function executes; contain local bindings.
- Class-definition namespaces: Created during class body execution; contents become class attributes.
- Builtins namespace: Contains built-in functions and exceptions, accessible globally.
During class body execution, the class namespace is separate. When the class is created, this namespace becomes the class's attribute dictionary. Method bodies, however, execute later with their own function local namespaces.
Examples demonstrating namespaces:
# Module namespace
print(globals()['x']) # Access module-level binding
def f():
# Function local namespace
print(locals())
class C:
pass
print(C.__dict__) # Class namespace with attributes
| Namespace Type | Creation Context | Typical Bindings | Persistence | Name Resolution Role |
|---|---|---|---|---|
| Module | Module import or run | Global variables, functions | Persistent for module lifetime | Global environment in execution |
| Function | Function call | Local variables, parameters | Exists during call | Local environment in execution |
| Class | Class body execution | Class attributes, methods | Persistent as class object | Temporary during class body exec |
| Annotation | Annotation evaluation | Type parameters, aliases | Temporary, lazy evaluated | Used during annotation resolution |
| Builtins | Python startup | Built-in functions, types | Persistent for interpreter lifetime | Fallback namespace in name resolution |
Namespace mappings exposed by introspection do not imply identical internal storage or management strategies across all Python implementations.
Name Binding in Python
Name binding establishes an association between a name and an object within a relevant namespace or binding environment. Binding differs from copying or mutating the referenced object; it merely attaches a name to an object.
Principal name-binding constructs include:
- Function parameters
- Function and class definitions
- Assignment statements and assignment expressions (
:=) - Loop variables and targets in
withand exception clauses - Pattern captures in structural pattern matching
- Imports
typestatements and type parameter lists
Deletion targets (del) are considered binding-relevant for local-name classification even though their runtime effect is to unbind the name.
Examples:
def f(param): # 'param' is bound as a parameter
x = 1 # 'x' is bound by assignment
import math # 'math' is bound by import
for y in range(3): # 'y' is bound in loop target
pass
with open('file') as fobj: # 'fobj' bound in with-statement
pass
try:
pass
except Exception as e: # 'e' bound in except clause
pass
Variables are classified as:
- Local variables: Bound within the current code block.
- Global variables: Bound at the module level.
- Free variables: Refer to bindings in an enclosing scope but not local to the current block. Distinct from undefined names.
The global statement redirects bindings and uses to the module-level namespace, while nonlocal refers to existing bindings in an enclosing function scope.
Example contrasting bindings:
x = 0 # module-level global
def outer():
x = 1 # enclosing function local
def inner():
nonlocal x # refers to `x` in outer()
x = 2 # rebinds `x` in outer()
def inner_global():
global x
x = 3 # rebinds module-level `x`
inner()
inner_global()
print(x) # prints 2 (from nonlocal rebind)
outer()
print(x) # prints 3 (global rebind)
Python Scope Model
Scope is the region of program execution in which a particular binding is directly visible through ordinary name resolution. Scope is distinct from the namespace that stores bindings.
Function scopes nest lexically: an inner function can access bindings from enclosing functions unless it defines a binding with the same name.
Module scope and the builtins namespace are the outermost scopes ordinarily reached after enclosing function scopes.
Class scope is special: names defined in a class body are available during the class body execution but do not become lexically enclosing local variables for methods, comprehensions, or generator expressions defined within the class body.
Examples:
def outer():
x = 10
def inner():
print(x) # accesses outer's x (enclosing scope)
inner()
class C:
y = 20
def method(self):
# print(y) # NameError: y not found in enclosing scopes
pass
Python does not create new lexical scopes for suites in control-flow statements (if, for, while, try, with). Names bound inside these suites belong to the containing scope.
| Scope Type | Enclosing Name Behavior | Important Exceptions |
|---|---|---|
| Module | No enclosing scopes; outermost | Global and builtins as fallbacks |
| Function | Lexically nested; can access enclosing | global and nonlocal declarations affect bindings |
| Class | No lexical closure for methods/comprehensions | Class body names visible during class execution only |
| Comprehension | Have their own implicit function scope | Do not inherit class body scope |
| Annotation | Annotation scopes behave like function scopes but can access immediate class namespace | Lazy evaluation and restrictions on yield etc. |
Scope rules are determined by language structure, not the temporal order in which statements execute.
Python Annotation Scopes
Annotation scopes are specialized execution scopes used during evaluation of annotations, type parameter lists, type statements, and related generic constructs in modern Python.
Introduced with modern generic syntax and expanded in Python 3.14, annotation scopes behave similarly to function scopes but can directly access names from the immediately enclosing class namespace, unlike ordinary methods or comprehensions.
Example (Python 3.14+):
class C:
T = int # name in class namespace
x: T # annotation scope can access T
def method(self):
# y: T # Error: ordinary function scope does not see class names lexically
pass
Annotation scopes impose restrictions:
yield,yield from,await, and assignment expressions (:=) are disallowed in expressions directly contained by annotation scopes.- Type parameters defined in annotation scopes cannot be rebound with
nonlocalfrom nested scopes. - Annotations and related constructs are lazily evaluated; expressions are executed only when their values are requested.
Example of lazy evaluation:
def f(x: '1 / 0'): # annotation is string, not evaluated immediately
pass
print(f.__annotations__) # accessing annotations triggers evaluation and error
| Aspect | Ordinary Function Scope | Annotation Scope |
|---|---|---|
| Access to class namespace | No | Yes |
| Lazy evaluation | No | Yes |
| Permitted expressions | All | No yield, await, := |
nonlocal behavior | Allowed | Type parameters cannot be rebound |
| Qualified-name effects | Normal | Lazy evaluation may delay errors |
Name Resolution in Python
Name resolution selects the binding associated with a name by searching the applicable execution environment according to scope rules.
For free variables, Python searches nearest enclosing scopes, then module globals, then builtins. Special class and annotation scopes modify this behavior.
If a name is bound anywhere in a function block, all ordinary uses of that name in the block are classified as local unless global or nonlocal declarations apply.
Example of local-before-binding error:
def f():
print(x) # UnboundLocalError because x is assigned later
x = 1
Corrected by declaring:
def f():
global x
print(x)
x = 1
NameError occurs when a name is unresolved, while UnboundLocalError occurs when a local variable is accessed before assignment.
Closure example showing runtime free-variable lookup:
x = 10
def f():
return x # free variable
x = 20
print(f()) # prints 20, binding resolved at call time
| Name Resolution Context | Search Order / Fallback | Error or Fallback Behavior |
|---|---|---|
| Local | Current function namespace | Access before assignment → UnboundLocalError |
| Free | Nearest enclosing scope | Unresolved → NameError |
| Global | Module namespace | Unresolved → NameError |
| Builtin | Builtins namespace | Unresolved → NameError |
| Class body | Class namespace during class exec | Not lexical closure for methods |
| Annotation scope | Annotation scope, enclosing class | Lazy evaluation, restrictions on expressions |
The dynamic execution of code with eval and exec uses supplied or caller namespaces but does not automatically gain the full lexical closure of the surrounding source code.
Exception Propagation in Python
Exception propagation transfers control from the point where an exception is raised through dynamically active execution contexts until a compatible handler is found or the exception remains unhandled.
Python's termination model means a handler continues execution at an outer level; it does not resume the failed operation at the exact point where the exception was raised.
Example:
def inner():
raise ValueError("error")
def middle():
inner()
def outer():
try:
middle()
except ValueError as e:
print("Caught:", e)
finally:
print("Cleanup")
outer()
Here, the exception raised in inner propagates through middle (which does not handle it) to outer, where it is caught. The finally block executes during unwinding.
Handler selection depends on exception class compatibility and propagates through active call frames, not lexical scopes.
If unhandled, exceptions produce traceback reports or return control to an interactive loop. Traceback representation is distinct from the propagation mechanism.
Python Runtime Structure
The Python runtime can be conceptualized as layered:
- Host machine: The physical or virtual hardware.
- Process: The operating system process executing Python.
- Python global runtime state: Interpreter-wide global state.
- Interpreter: An isolated execution environment with persistent state.
- Host thread: Operating system thread executing Python code.
- Python thread-specific state: Python interpreter state specific to a thread.
Implementations may vary in how they materialize these layers. Multiple Python interpreters can exist in a single process. Each interpreter may be used by one or more Python thread states. Processes, interpreters, host threads, Python thread states, execution frames, and asynchronous tasks are distinct concepts.
| Layer | Isolation | Lifetime | Principal State | Relation to Executing Code |
|---|---|---|---|---|
| Process | Fully isolated | Process lifetime | OS process state | Hosts interpreters and threads |
| Python global runtime | Shared across interpreters | Process lifetime | Global interpreter state | Shared interpreter resources |
| Interpreter | Isolated | Interpreter lifetime | Interpreter-specific state | Executes Python code |
| Host thread | OS thread | Thread lifetime | OS thread context | Runs Python thread state |
| Python thread state | Per-thread | Thread lifetime | Thread-specific interpreter state | Manages execution frames |
| Execution frame | Per-call | Execution duration | Local execution context | Executes one code block |
Solved Python Execution Model Exercise
(Exercise content can be created here following the model if requested.)