Python Execution Model

The execution model is the language-level set of rules — defined in §4 of the Python language reference — that govern how a program is structured into code blocks, how names come to refer to objects (binding), how a used name is resolved to the right object (scope), and how exceptions interrupt normal control flow. It is the contract every Python implementation must honour, sitting above any particular interpreter: it says what x means at a given point in the text, not how the bytecode looks up x (that mechanism — fast locals, cell variables, LOAD_GLOBAL — lives in The CPython Evaluation Loop and the detailed scope-chain note Local Global and Nonlocal Scopes). The single most important rule is that Python decides whether a name is local, enclosing, global, or built-in by statically scanning the whole text of a block for binding operations before it ever runs a line — which is why a name used before its assignment in the same function raises UnboundLocalError, not a fall-through to a global.

Scope of this note

This note covers the language-level model: code blocks, binding, name resolution, builtins, and exceptions as the reference manual defines them. The detailed LEGB scope-chain mechanics and the global/nonlocal traps belong to Local Global and Nonlocal Scopes and The Global and Nonlocal Statement Traps; annotation scopes and lazy annotation evaluation belong to Deferred Annotations; closures and cell variables to Cell Variables and Closures; and the runtime/thread-state layering (§4.4 of the reference) to Thread State and the GIL Handshake and Subinterpreters. This note links out to those rather than duplicating them.

Mental Model: Blocks, Bindings, and a Static Scan

Think of the execution model in three layers. At the bottom, the program text is divided into blocks — a module, a function body, a class body. Each block, when run, gets an execution frame and a namespace (a mapping from names to objects). The middle layer is binding: certain syntactic constructs (assignment, def, class, import, function parameters, for targets, with ... as, and more) bind a name in the current block’s namespace. The top layer is resolution: when a name is used, Python must decide which namespace to read it from — and it makes that decision largely at compile time, by scanning the entire block for binding operations.

graph TD
    TEXT["Source text of a block"] --> SCAN["Compile-time scan:<br/>find every binding op in the block"]
    SCAN --> CLASS{"For each name:<br/>bound in this block?<br/>declared global/nonlocal?"}
    CLASS -->|"bound, no decl"| LOCAL["LOCAL to this block"]
    CLASS -->|"declared global"| GLOBAL["GLOBAL (module namespace)"]
    CLASS -->|"declared nonlocal"| ENCL["ENCLOSING function scope"]
    CLASS -->|"used, never bound here"| FREE["FREE variable<br/>(resolved in enclosing/global/builtins)"]
    LOCAL --> RUN["At runtime: read/write the chosen namespace;<br/>local-before-bind ⇒ UnboundLocalError"]
    GLOBAL --> RUN
    ENCL --> RUN
    FREE --> RUN

Diagram: how a name’s category is decided. The key insight is the left-to-right time order — the category (local vs. global vs. enclosing vs. free) is fixed by a compile-time scan of binding operations, and only the value is looked up at runtime. This is why moving an assignment to the bottom of a function retroactively makes every earlier use of that name local, turning a previously-working global read into an UnboundLocalError.

Structure of a Program: What Counts as a Block

The reference defines a block as “a piece of Python program text executed as a unit.” The following are blocks (executionmodel §4.1): a module; a function body; a class definition; each command typed interactively; a script file (or standard input); a script given with the -c option; a module run as a top-level script with -m (its name becomes __main__); and the string arguments passed to eval() and exec(). Each block “is executed in an execution frame,” which “contains administrative information (used for debugging) and determines where execution continues after the block completes.”

The practical consequence is that blocks are the unit of scope. A module, a function, and a class each introduce their own namespace; ordinary statement groupings — the body of an if, a for, a while, a try, or a with — do not create a new block or namespace. A variable assigned inside an if is just a variable of the enclosing function or module; there is no block-level scoping in Python the way there is in C or Java. (The one subtlety — that comprehensions and generator expressions do get their own implicit function scope — is covered under Comprehension Scoping.)

Naming and Binding

A name “refers to objects” and is “introduced by name binding operations.” The reference enumerates the constructs that bind names (§4.2.1):

  • Formal parameters to functions;
  • class definitions (the class name) and def definitions (the function name);
  • Assignment expressions (the walrus, :=) and ordinary assignment targets that are identifiers;
  • Identifier targets in for loop headers;
  • The name after as in with statements, except/except* clauses, and structural-pattern-matching as-patterns;
  • Capture patterns in structural pattern matching;
  • import statements;
  • type statements and type-parameter lists (the generics syntax added in 3.12).

The from module import * form is a special case: at module level it binds “all names defined in the imported module except those beginning with underscore.” A del target is, for scope-classification purposes, “considered bound” — i.e. del x still makes x a local of the block even though it semantically unbinds it.

These give the three categories the rest of the model rests on, in the spec’s own words: a name “bound in a block and not declared nonlocal or global” is a local variable of that block; a name “bound at module level” is a global variable; and a name “used in a code block but not defined there” is a free variable.

The exact binding effect of the workhorse statements is worth pinning to the reference for the simple statements:

  • Assignment. For an identifier target, “if the name does not occur in a global or nonlocal statement in the current code block: the name is bound to the object in the current local namespace. Otherwise: the name is bound to the object in the global namespace or the outer namespace determined by nonlocal, respectively.” Rebinding an already-bound name may drop the previous object’s refcount to zero and deallocate it (the Reference Counting hook).
  • del. “Deletion of a name removes the binding of that name from the local or global namespace, depending on whether the name occurs in a global statement in the same code block.” Deleting an already-unbound name raises NameError.
  • import. Binds the imported module (or, for import foo.bar.baz without as, the top-level package foo) into the local namespace; from foo import attr binds attr (or its as-alias) directly.

Resolution of Names: The Scope Chain at a High Level

When a name is used, “it is resolved using the nearest enclosing scope. The set of all such scopes visible to a code block is called the block’s environment.” Informally this is the LEGB rule — Local, then Enclosing function scopes, then Global (module), then Built-in — and the detailed mechanics live in Local Global and Nonlocal Scopes. Here it suffices to state the language-level rules the model itself defines.

The early-binding rule (the heart of the model)

The rule that surprises everyone is stated plainly in the reference: “If a name binding operation occurs anywhere within a code block, all uses of the name within the block are treated as references to the current block.” Python scans the whole text of a block for binding operations before execution to decide which names are local. So this fails:

def f():
    print(x)   # UnboundLocalError: x is local (bound below), but not yet assigned
    x = 5

Because x = 5 appears somewhere in f, x is a local of f everywhere in f — including the earlier print(x), where it has not yet been given a value. The reference defines exactly this case: UnboundLocalError (a subclass of NameError) is raised “when the current scope is a function scope and the name refers to a local variable that has not yet been bound to a value at the point where it is used.” If the name is not local to the block and is simply absent everywhere, the plain NameError is raised instead. (The interaction of this rule with global/nonlocal and the mistakes it causes are the subject of The Global and Nonlocal Statement Traps.)

global and nonlocal

These two statements override the default local classification. The global statement “causes the listed identifiers to be interpreted as globals” and “applies to the entire current scope”; without it you cannot assign to a module global from inside a function (though you can freely read one as a free variable). At module level it is a no-op since “all variables are global.” The nonlocal statement “causes the listed identifiers to refer to names previously bound in nonlocal scopes” — the local scopes of enclosing functions — and crucially requires the name to already exist there: “if a name is not bound in any nonlocal scope, or if there is no nonlocal scope, a SyntaxError is raised.” Both raise SyntaxError “if a variable is used or assigned to prior to its [global/nonlocal] declaration in the scope.” Full mechanics: Local Global and Nonlocal Scopes.

The class-scope exception

Class bodies are special: “the scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods. This includes comprehensions and generator expressions.” A class namespace is not an enclosing scope for the functions (or comprehensions) defined inside it. Hence the classic failure:

class A:
    a = 42
    b = [a + i for i in range(10)]   # NameError: 'a' not visible in the comprehension's scope

The comprehension runs in its own implicit function scope nested in the class body, and because class scope is skipped, a is not found. The fix is to refer to A.a after the class exists, or to avoid the nested scope. (Annotation scopes are a deliberate exception to this exception — they can see the class namespace — but that belongs to Deferred Annotations.)

Resolution happens at runtime, for free variables

For a free variable, the category is fixed at compile time but the value is read at runtime, so reassigning a global between definition and call is visible:

i = 10
def f():
    print(i)   # i is a free variable, resolved when f() runs
i = 42
f()            # prints 42, not 10

When a name is neither local, enclosing, nor global, Python consults the built-in namespace — the source of print, len, range, Exception, and so on. The reference describes the mechanism precisely: “the builtins namespace associated with the execution of a code block is actually found by looking up the name __builtins__ in its global namespace; this should be a dictionary or a module.” In the __main__ module, __builtins__ is the built-in module builtins itself; in every other module it is “an alias for the dictionary of the builtins module.” The manual is blunt that this is plumbing, not API: “users should not touch __builtins__; it is strictly an implementation detail.” The takeaway is that the built-in scope is not magic — it is one more namespace, reachable (and, in principle, replaceable per-module) through __builtins__, which is exactly the hook sandboxing experiments have historically abused.

The reference also notes that eval() and exec() “do not have access to the full environment for resolving names”: they see the caller’s local and global namespaces, but “free variables are not resolved in the nearest enclosing namespace, but in the global namespace” — a deliberate limitation, since a dynamically-compiled string has no static enclosing-scope information to bind against.

Exceptions as Part of the Model

The execution model includes exceptions because they are a form of control flow, not merely an error-reporting convenience. The reference: “exceptions are a means of breaking out of the normal flow of control of a code block in order to handle errors or other exceptional conditions. An exception is raised at the point where the error is detected; it may be handled by the surrounding code block or by any code block that directly or indirectly invoked the code block where the error occurred.”

The defining design choice is the termination model: “Python uses the ‘termination’ model of error handling: an exception handler can find out what happened and continue execution at an outer level, but it cannot repair the cause of the error and retry the failing operation (except by re-entering the offending piece of code from the top).” This contrasts with a resumption model (as in some Lisps/Smalltalk condition systems) where a handler can fix the problem and resume at the point of the error. Python cannot: once unwinding starts, the failing operation’s frame is gone; the only way to “retry” is to loop back and run the code afresh.

A few further model-level facts: exceptions may be raised by the interpreter on run-time errors (e.g. division by zero) or explicitly via raise; they are “identified by class instances,” and an except clause matches “based on the instance’s class or non-virtual base classes”; an unhandled exception terminates the interpreter and prints a traceback (except for SystemExit); and “exception messages are not part of the Python API,” so their wording “may change from one version of Python to the next.” The implementation of all this — zero-cost exception tables, the traceback object, stack unwinding through frames — is covered in Exception Handling Internals, Zero-Cost Exception Handling, and Traceback Objects and Stack Unwinding.

Common Misunderstandings

  • “A name is local only after the assignment runs.” No — it is local for the entire block the moment a binding operation appears anywhere in that block’s text. The early-binding scan is textual and static, which is the whole reason for UnboundLocalError.
  • if/for/while/with blocks have their own scope.” They do not. Only modules, functions, and classes introduce namespaces. A variable assigned in an if leaks to the rest of the function — by design.
  • “Class attributes are visible inside methods without self/ClassName.” No — class scope does not extend into method or comprehension bodies. Reference them through self. or ClassName..
  • “Reading a global from a function needs global.” Reading does not; only assigning (rebinding the module-level name) requires global. Without it, an assignment silently creates a local instead — and any earlier read becomes an UnboundLocalError.
  • __builtins__ is the same everywhere.” It is a module in __main__ but a dict elsewhere; treat it as an implementation detail and do not rely on either form.

See Also