Python: Scope and Namespaces
Understanding how Python finds names is the key to debugging a surprising category of errors. Once the LEGB rule clicks, you stop being confused by why a variable is not what you expect, and you start writing closures and decorators with confidence.
What is a Namespace?
A namespace is a mapping from names to objects. Think of it as a dictionary where the keys are variable names (strings) and the values are the actual Python objects those names point to. Python uses namespaces to keep names in different contexts separate: a variable called total inside one function has nothing to do with a variable called total in another function, because they live in different namespaces.
Python maintains several namespaces at any point during execution. A scope is the region of code where a particular namespace is directly visible without any qualification. The two concepts are closely related but distinct: the namespace is the data structure, and the scope is the textual region of code that has access to it.
Python's Four Namespaces
Each namespace has a different lifetime and region of visibility.
- Built-in: created when the interpreter starts, contains len, print, range, True, None, and all other built-ins. It never goes away.
- Global (module): created when a module is imported or a script starts. Contains the top-level names of that module.
- Enclosing (nonlocal): exists only for nested functions. It is the local scope of any enclosing function.
- Local (function): created when a function is called and destroyed when it returns. Contains the function's own variables and parameters.
The LEGB Rule: How Python Resolves Names
Whenever Python encounters a name, it looks it up in a fixed sequence of scopes. The order is Local, then Enclosing, then Global, then Built-in. This is called the LEGB rule. The search stops as soon as Python finds the name in one of these scopes. If the name is not found in any of them, Python raises aNameError.
Knowing this order explains a common source of confusion. If you define a local variable with the same name as a global or built-in, the local one takes precedence inside that function. The global or built-in is still there; it is just shadowed.
LEGB in Action
PythonEach scope found in the lookup order, shown with a concrete example.
Shadowing a Built-in
PythonDefining a local name that matches a built-in hides the built-in.
The global Keyword
Reading a global variable from inside a function works without any declaration. But the moment Python sees an assignment to a name inside a function, it treats that name as local for the entire function, even lines above the assignment. This causes an UnboundLocalError if you try to read the name before the assignment.
The global statement tells Python that a given name, regardless of assignment, refers to the module-level global and not a new local variable. Use it sparingly. Relying heavily on mutable global state makes code hard to test and reason about. Functions that communicate through arguments and return values are almost always the better design.
Reading vs Modifying a Global Variable
PythonWhy reading works but assignment without global creates a local.
The nonlocal Keyword
nonlocal solves the same problem as global but for the enclosing function scope rather than the module scope. Without it, an inner function can read from the enclosing scope but cannot assign to its variables. Declaring a name as nonlocal tells Python to look for it in the nearest enclosing scope that is not global and treat any assignment as a modification of that existing binding.
This is the mechanism that makes stateful closures work. Counters, accumulators, and memoisation caches in closures almost always rely on nonlocal to update their captured state.
nonlocal for Stateful Closures
PythonAn inner function that modifies the enclosing scope's counter.
nonlocal with Multiple Nesting Levels
Pythonnonlocal resolves to the nearest enclosing scope that holds the name.
__builtins__
The built-in scope is implemented as a module called builtins. Python makes it accessible in every module via the name __builtins__. In the main script,__builtins__ is the module object itself. In imported modules, it is typically the module's __dict__. This inconsistency is a CPython implementation detail, so you should import builtins explicitly if you need reliable access to it.
The most practical use of this knowledge is recovering a shadowed built-in. If you or a library have accidentally overwritten list, open, or another built-in, you can retrieve the original from builtins.
Accessing and Restoring Built-ins
PythonUsing the builtins module to work around a shadowed name.
vars(), dir(), locals(), and globals()
Python gives you several built-in tools to inspect namespaces at runtime. Each one answers a slightly different question, and picking the right one avoids confusion.
Introspection Functions at a Glance
Four functions, four distinct purposes.
- locals(): returns a snapshot of the current local namespace as a dictionary. Modifying this dict does NOT affect actual local variables.
- globals(): returns the current module's global namespace as a live dictionary. Modifications here DO affect the module.
- vars(): called with no argument, behaves like locals(). Called with an object, returns that object's __dict__.
- dir(): called with no argument, returns names in the current scope. Called with an object, returns a sorted list of its attributes and methods.
Exploring Namespaces at Runtime
PythonUsing introspection functions to see what Python can see.
Module Namespaces
Every module in Python is its own namespace. When you write import math, Python creates a module object and assigns it to the name math in your current namespace. All of math's functions, constants, and classes live inside that module's namespace, accessible as math.sqrt, math.pi, and so on. They never pollute your module's namespace.
When you use from math import sqrt, Python copies the name sqrtinto your current namespace. The module object still exists, but you have no reference to it unless you also do import math. This is whyfrom module import * is discouraged: it dumps all of the module's public names into your namespace, which can cause silent collisions and makes it hard to tell where a name came from.
Module Namespaces and Import Styles
PythonHow different import forms affect what lands in your namespace.
__name__ and the Main Guard
PythonWhy every Python script checks if __name__ == '__main__'.
Name Mangling (__name)
Python does not have truly private attributes. By convention, a single leading underscore (_name) signals that something is intended for internal use and should not be part of the public API. Tools and IDEs respect this convention, but Python does not enforce it.
A double leading underscore (__name) triggers a feature called name mangling. Python rewrites the attribute name by prepending the class name: __balance in class Account becomes_Account__balance. This is not access control; the attribute is still reachable. The purpose is to prevent accidental name collisions when subclasses add attributes with the same name, not to enforce privacy.
Name Mangling in Practice
PythonHow Python renames double-underscore attributes to avoid subclass collisions.
Inspecting Mangled Names
PythonUsing dir() to see exactly what Python has renamed.
Putting It All Together
The example below walks through all four LEGB scopes in a single piece of code and uses global, nonlocal, and the introspection functions in a way that shows how they relate. Reading through it step by step and predicting each output before running it is one of the most effective exercises for making scope truly intuitive.
All Scope Concepts in One Example
PythonLEGB, global, nonlocal, and introspection working together.
Quiz - Test Your Knowledge
Ten questions covering namespaces, the LEGB lookup order, the behaviour ofglobal and nonlocal, introspection functions, module namespaces, and name mangling. Several questions test the difference between what Python allows and what it does by default.
Knowledge Check
1. What is a namespace in Python?
2. In what order does Python resolve a name under the LEGB rule?
3. What happens if you try to assign to a global variable inside a function without declaring it with the global keyword?
4. What does the nonlocal keyword do?
5. Which function returns a dictionary of all names in the current local scope?
6. What does dir(obj) return when called with an argument?
7. When you import a module with "import math", what namespace does it create?
8. What does Python's name mangling transform __attr into?
9. What is the built-in scope in Python?
10. What does vars(obj) return when called with an object argument?