Python Namespace and Scope Internals

Learn how Python resolves names using the LEGB rule, how __dict__ stores attributes, and how closures capture variables from enclosing scopes.

8 min read

Namespaces and scopes are the systems Python uses to track which names exist and where they can be accessed. A namespace is a mapping from names to objects. A scope is a region of code where a namespace is directly accessible.

The article on attribute lookup in Python covered how Python resolves attributes on objects. This article covers name resolution in general: how Python decides what a bare name like x refers to, how functions create local scopes, and how closures capture variables from enclosing functions.

Understanding namespaces and scopes is essential for debugging NameError and UnboundLocalError, writing correct closures in loops, and using the global and nonlocal statements effectively.

The LEGB rule

Python resolves names by searching four scopes in a fixed order. This is called the LEGB rule, short for Local, Enclosing, Global, Built-in.

The local scope is the current function. Names defined inside the function by assignment or as parameters are local. The enclosing scope applies to nested functions and contains names from outer functions.

The global scope is the module level. Names defined at the top of a module or declared global inside a function are global. The built-in scope contains names like print, len, and range that are always available.

pythonpython
x = "global"
 
def outer():
    x = "enclosing"
 
    def inner():
        x = "local"
        print(x)
 
    inner()
 
outer()

The outer function creates the enclosing scope. The inner function creates the local scope and prints x from that innermost scope.

texttext
local

Remove the local assignment and Python finds the enclosing version. Remove that too and Python reaches the global. This cascading resolution is the LEGB rule in action.

How namespaces are stored

Every scope in Python is backed by a dictionary. The local and global scopes can be inspected with the locals and globals built-in functions.

pythonpython
top_level = "module scope"
 
def show_namespaces():
    local_var = "inside"
    print("Locals:", list(locals().keys()))
    print("Globals has top_level:", "top_level" in globals())
    print("Globals has print:", "print" in globals())
 
show_namespaces()

The locals call returns the current function namespace dictionary with all local variables. The globals call returns the module-level namespace, which holds top_level but not print, since print lives in the separate built-in scope rather than the global one.

texttext
Locals: ['local_var']
Globals has top_level: True
Globals has print: False

Object namespaces are stored in the dict attribute. When you access obj.x, Python looks in type(obj).dict for descriptors and in obj.dict for instance attributes.

pythonpython
class Point:
    origin = (0, 0)
 
p = Point()
p.x = 10
print(Point.__dict__.keys())
print(p.__dict__)

The class dict contains origin and various metadata methods. The instance dict contains only the x attribute that was set directly on the instance.

texttext
dict_keys(['__module__', '__firstlineno__', 'origin', '__static_attributes__', '__dict__', '__weakref__', '__doc__'])
{'x': 10}

How closures capture variables

A closure is a nested function that references variables from an enclosing function after the enclosing function has returned. Python stores these captured variables in cell objects rather than in the enclosing function's local namespace.

pythonpython
def make_counter():
    count = 0
 
    def counter():
        nonlocal count
        count += 1
        return count
 
    return counter
 
c1 = make_counter()
print(c1())
print(c1())
print(c1())

The counter function references count from make_counter. The nonlocal declaration is required because the function assigns to count. Without nonlocal, Python would treat count as a local variable.

texttext
1
2
3

Each call to make_counter creates a new cell for count. Multiple closures from the same factory do not share state.

pythonpython
c2 = make_counter()
print(c1())
print(c2())

The two counters are independent because each call to make_counter creates a fresh count cell and a new closure that references it.

texttext
4
1

You can inspect closure cells through the closure attribute on the function object. Each cell contains the current value of a captured variable.

The global and nonlocal statements

By default, assigning to a name inside a function creates a local variable. The global statement tells Python to assign to a global variable instead. The nonlocal statement tells Python to assign to a variable in an enclosing scope.

pythonpython
total = 0
 
def accumulate(value):
    global total
    total += value
 
accumulate(10)
accumulate(5)
print(total)

Without the global declaration, total += value would create a local variable and fail because a local named total would not have an initial value.

texttext
15

Nonlocal is required when a nested function assigns to an enclosing variable. Reading an enclosing variable does not require nonlocal. Only assignment requires the declaration.

pythonpython
def outer():
    message = "hello"
 
    def reader():
        return message
 
    def writer():
        nonlocal message
        message = "updated"
 
    writer()
    return reader()
 
print(outer())

The reader function reads message without nonlocal. The writer function assigns to it and must declare nonlocal to avoid creating a local shadow.

texttext
updated

The article on Python memory management covers how closure cells affect object lifetimes and garbage collection.

Rune AI

Rune AI

Key Insights

  • Python resolves names using the LEGB order: Local, Enclosing, Global, Built-in.
  • The global and nonlocal statements change where assignments store their target.
  • Object namespaces are stored in the dict attribute as regular dictionaries.
  • Closures use cell objects to share variables between enclosing and nested functions.
  • The locals() and globals() functions return the current scope dictionaries for inspection.
RunePowered by Rune AI

Frequently Asked Questions

What is the LEGB rule in Python?

LEGB stands for Local, Enclosing, Global, Built-in. It is the order Python follows when resolving a name. First check the local scope inside the current function. Then check any enclosing functions from inner to outer for nested functions. Then check the module-level global scope. Finally check the built-in namespace that contains functions like print and len.

How do Python closures capture variables?

Closures capture variables from enclosing scopes using cell objects. When a nested function references a variable from an enclosing function, Python stores the variable in a cell object rather than in the enclosing function's local namespace. Both the enclosing function and the nested function access the same cell, which means the nested function sees the variable's current value, not the value it had when the nested function was defined.

Conclusion

Python's namespace and scope system is the mechanism behind every name lookup in the language. The LEGB rule provides a predictable resolution order. The dict attribute on objects stores instance namespaces. Closure cells capture enclosing variables by reference. Understanding these internals makes you better at debugging NameErrors, writing correct closures, and reasoning about variable lifetimes.