21.6 Scope Pitfalls: UnboundLocalError and Late-Binding Closures

One of the most common and initially confusing issues encountered by Python programmers is the UnboundLocalError. This error arises from a misunderstanding of how Python’s scoping rules interact with variable assignment. The core principle is that any variable to which a value is assigned anywhere within a function body is treated as a local variable for the entire scope of that function, unless explicitly declared otherwise. The UnboundLocalError Explained This error occurs when a local variable is referenced before it has been assigned a value. The confusion often stems from the fact that a variable of the same name exists in an enclosing scope, leading the programmer to believe the inner function will use that value. However, because an assignment exists later in the function, Python binds the variable name to the local scope from the outset. When the code tries to read from this local variable before it has been assigned, the UnboundLocalError is raised.

21.5 The Built-in Namespace: __builtins__

The built-in namespace is the final frontier in Python’s LEGB lookup chain, housing the language’s fundamental constructs. This namespace is not a typical module but a special collection of objects that are available everywhere, without any explicit import. This universal accessibility is crucial because it provides the foundational tools upon which all Python code is built. When the interpreter cannot resolve a name in the local, enclosing, or global namespaces, it turns to this built-in repository as its last resort.

21.4 How Python Resolves Names at Compile Time vs Runtime

In Python, name resolution is governed by the LEGB rule, a fundamental concept that dictates the order in which the interpreter searches for a name’s value. LEGB stands for Local, Enclosing, Global, Built-in, representing the hierarchy of scopes Python checks. Crucially, this process involves two distinct phases: compile-time and runtime, each playing a different role in how names are bound and looked up. Compile-Time: Bytecode Generation and Name Categorization When a Python module is imported or a script is run, it is first compiled into bytecode. During this compilation phase, the Python interpreter analyzes the code statically (without executing it) to determine the scope of each name. It categorizes every variable reference as either local, global, or free. This categorization is permanently baked into the generated bytecode and dictates the opcode used to fetch the name’s value later at runtime.

21.3 global and nonlocal Declarations

In Python, variable resolution follows the LEGB rule: Local, Enclosing, Global, Built-in. This systematic search order ensures predictable behavior, but sometimes we need to explicitly instruct the interpreter to modify a variable from a non-local scope rather than create a new local one. This is where the global and nonlocal declarations become essential. They are explicit statements that break the default read-only relationship with variables in outer scopes, allowing assignment operations to affect those specific namespaces directly.

21.2 Local, Enclosing, Global, and Built-in Scopes

In Python, the concept of scope defines the region of a program where a particular name (like a variable or function) is accessible and can be referenced. The rules that determine this accessibility are collectively known as the LEGB rule, which is a mnemonic for the order in which Python searches for a name: Local, Enclosing, Global, Built-in. Understanding this hierarchy is fundamental to writing predictable, bug-free code. The Four LEGB Scopes The LEGB rule describes a chain of scopes that Python traverses, in order, to resolve a name.

21.1 What Is a Namespace?

In Python, a namespace is a fundamental concept that acts as a mapping from names (identifiers) to objects. It is the system that Python uses to ensure that every name in a program is unique and can be accessed without conflict. You can think of it as a dictionary where the keys are the variable, function, and class names you define, and the values are references to the corresponding objects in memory. This abstraction is crucial for organizing code, preventing naming collisions, and enabling modular programming.

— joke —

...