✦ For everyone, free.

Practical knowledge for real and everyday life

Home

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.

ConceptExecution-Model Responsibility
Code BlockDefines a unit of executable Python code text
FrameHolds runtime context and manages execution state for a code block
NamespaceMaps names to objects within a given context
BindingAssociates a name with an object in a namespace
ScopeDefines where bindings are directly visible through name resolution
EnvironmentThe collection of accessible scopes and namespaces at a point in execution
InterpreterMaintains persistent interpreter-specific state and executes code
Thread StateTracks thread-specific execution state and manages concurrency
Interpreter Thread State Execution Frame Code Block (module, function, class) Namespaces local, global, builtins uses executes in belongs to runs within

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 -c option.
  • Modules executed as top-level programs via -m.
  • Source strings supplied to eval or exec.

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 C is a separate code block.
  • The function body of method is a separate code block.
  • The if statement body inside method is 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:

AttributeDescription
f_codeThe code object currently being executed
f_localsDictionary mapping local variable names to objects
f_globalsDictionary mapping global names to objects
f_builtinsDictionary mapping built-in names to objects
f_backReference to the previous (caller) frame object
Execution positionInformation 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 TypeCreation ContextTypical BindingsPersistenceName Resolution Role
ModuleModule import or runGlobal variables, functionsPersistent for module lifetimeGlobal environment in execution
FunctionFunction callLocal variables, parametersExists during callLocal environment in execution
ClassClass body executionClass attributes, methodsPersistent as class objectTemporary during class body exec
AnnotationAnnotation evaluationType parameters, aliasesTemporary, lazy evaluatedUsed during annotation resolution
BuiltinsPython startupBuilt-in functions, typesPersistent for interpreter lifetimeFallback 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 with and exception clauses
  • Pattern captures in structural pattern matching
  • Imports
  • type statements 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 TypeEnclosing Name BehaviorImportant Exceptions
ModuleNo enclosing scopes; outermostGlobal and builtins as fallbacks
FunctionLexically nested; can access enclosingglobal and nonlocal declarations affect bindings
ClassNo lexical closure for methods/comprehensionsClass body names visible during class execution only
ComprehensionHave their own implicit function scopeDo not inherit class body scope
AnnotationAnnotation scopes behave like function scopes but can access immediate class namespaceLazy 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 nonlocal from 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
AspectOrdinary Function ScopeAnnotation Scope
Access to class namespaceNoYes
Lazy evaluationNoYes
Permitted expressionsAllNo yield, await, :=
nonlocal behaviorAllowedType parameters cannot be rebound
Qualified-name effectsNormalLazy 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 ContextSearch Order / FallbackError or Fallback Behavior
LocalCurrent function namespaceAccess before assignment → UnboundLocalError
FreeNearest enclosing scopeUnresolved → NameError
GlobalModule namespaceUnresolved → NameError
BuiltinBuiltins namespaceUnresolved → NameError
Class bodyClass namespace during class execNot lexical closure for methods
Annotation scopeAnnotation scope, enclosing classLazy 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.

LayerIsolationLifetimePrincipal StateRelation to Executing Code
ProcessFully isolatedProcess lifetimeOS process stateHosts interpreters and threads
Python global runtimeShared across interpretersProcess lifetimeGlobal interpreter stateShared interpreter resources
InterpreterIsolatedInterpreter lifetimeInterpreter-specific stateExecutes Python code
Host threadOS threadThread lifetimeOS thread contextRuns Python thread state
Python thread statePer-threadThread lifetimeThread-specific interpreter stateManages execution frames
Execution framePer-callExecution durationLocal execution contextExecutes one code block

Solved Python Execution Model Exercise

(Exercise content can be created here following the model if requested.)