Skip to content

Scope, Lifetime, & The LEGB Rule

Scope determines where a variable is visible and accessible within your code. Lifetime determines how long that variable exists in memory. Failing to understand these rules leads to the most common bugs in software: name collisions and NameError exceptions.


When you reference a variable name, Python searches for it in four specific locations, in this exact order:

  1. Local: Inside the current function (including parameters).
  2. Enclosing: Inside any “outer” functions (if using nested functions).
  3. Global: At the top level of your script or module.
  4. Built-in: Python’s pre-defined names (e.g., len, int, print).

If Python reaches the “B” and still can’t find the name, it raises a NameError.


2. Modifying Outer Scopes: global and nonlocal

Section titled “2. Modifying Outer Scopes: global and nonlocal”

By default, functions can read outer variables but cannot modify them. If you try to assign a value to a global name, Python simply creates a new local variable with that same name (Shadowing).

Used to modify a variable defined at the top level of the module.

count = 0
def increment():
global count
count += 1

Used in nested functions to modify a variable in the enclosing function’s scope.

nonlocal_example.py
def outer():
x = "Initial"
def inner():
nonlocal x
x = "Modified"
inner()
print(x) # Output: Modified

3. Under the Hood: Frame Objects & The Stack

Section titled “3. Under the Hood: Frame Objects & The Stack”

How does Python actually manage these scopes? Every time you call a function, Python creates a Frame Object on the Call Stack.

  1. Creation: The frame holds the function’s local variables (the “Namespace”).
  2. Execution: Python’s PVM executes the bytecode within that frame.
  3. Destruction: When the function returns, the frame is “popped” off the stack. All local variables in that frame are deleted (decremented in reference count).

If an inner function is returned from an outer function, the inner function “captures” the enclosing frame. This is a Closure, and it’s why the inner function can still access outer variables even after the outer function has finished executing.


Shadowing occurs when you name a local variable the same as a global variable or a built-in function.

shadowing.py
len = 10 # Shadowing the built-in len() function
def calc():
x = 5 # Shadowing? No, just a local.
# print(len([1, 2])) # TypeError: 'int' object is not callable

ScopeDefined Where?LifetimeAccess Level
LocalInside a function.Duration of the call.Read/Write (local).
EnclosingInside outer function.Until inner function is deleted.Read (Write with nonlocal).
GlobalTop level of .py file.Duration of the program.Read (Write with global).
Built-inPython Interpreter.Always available.Read-Only.