🕯️ Magic Note
Python uses the LEGB rule to resolve variable names: Local, Enclosing, Global, Built-in. When you use a variable name, Python searches in this order. First the local function scope. Then any enclosing functions (functions inside functions). Then the global module scope. Finally the built-in scope (like print and len). This explains why you can use len() without defining it.
Python
def my_function():
x = 10 # Local variable
print(f”Inside function: {x}”)
my_function() # Inside function: 10
# print(x) # NameError: name ‘x’ is not defined
Python
message = “Hello, world!” # Global variable
def show_message():
print(message) # Can READ global variable
show_message() # Hello, world!
Python
count = 0 # Global variable
def increment():
count = count + 1 # ERROR! count on right is local, but uninitialized
print(count)
# increment() # UnboundLocalError: local variable ‘count’ referenced before assignment
# Why? Python sees the assignment and treats ‘count’ as local.
# But then you try to read it before assigning a value.
Python
counter = 0 # Global variable
def increment():
global counter # Declare that we are using the global counter
counter += 1
print(f”Counter: {counter}”)
increment() # Counter: 1
increment() # Counter: 2
increment() # Counter: 3
print(f”Outside: {counter}”) # Outside: 3
🕯️ Magic Note
Using global is often discouraged. Functions that modify global variables are harder to test and debug. They create hidden dependencies. A better approach is to pass values as parameters and return results. Use global sparingly, for truly global state like configuration or counters that must persist across many functions.
Python
def outer():
x = 10 # Enclosing variable
def inner():
print(f”Inner sees x: {x}”) # Can read enclosing variable
inner()
outer() # Inner sees x: 10
Python
def outer():
x = 10 # Enclosing variable
def inner():
nonlocal x # Declare that we want to modify the enclosing x
x += 5
print(f”Inner: {x}”)
inner()
print(f”Outer after inner: {x}”)
outer()
# Inner: 15
# Outer after inner: 15
🕯️ Magic Note
nonlocal is like global but for enclosing function scopes. It is used in closures and decorators. Without nonlocal, you can read enclosing variables but not assign to them. Python would create a new local variable instead.
Python
# Built-in functions are always available
print(len(“hello”)) # 5
print(type(42)) # <class ‘int’>
# But you can shadow them (not recommended)
len = 100 # Now len is an integer, not the function!
# print(len(“hello”)) # TypeError: ‘int’ object is not callable
# To restore shadowed built-ins, you can delete the variable
del len
print(len(“hello”)) # 5 (back to normal)
Python
x = “global” # Global
def outer():
x = “enclosing” # Enclosing
def inner():
x = “local” # Local
print(f”Inner sees: {x}”)
inner()
print(f”Outer sees: {x}”)
outer()
print(f”Global sees: {x}”)
# Inner sees: local
# Outer sees: enclosing
# Global sees: global
| Scope | Lifetime |
|---|---|
| Local | Created when function is called, destroyed when function returns |
| Enclosing | Lives as long as the outer function is executing |
| Global | Lives from module load until program ends |
| Built-in | Lives for entire program execution |
Python
def create_counter():
count = 0 # Local to create_counter
def counter():
nonlocal count
count += 1
return count
return counter # Returns a closure
my_counter = create_counter()
print(my_counter()) # 1
print(my_counter()) # 2
print(my_counter()) # 3
# The count variable lives on because the inner function references it
# This is a closure. The variable is enclosed even after outer function ends.
🕯️ Magic Note
Closures extend the lifetime of variables. Normally, local variables are destroyed when a function ends. But if an inner function references them, they are kept alive. This is how decorators and closures work.
Python
# Loop variable is accessible outside the loop (unlike some languages)
for i in range(5):
pass
print(i) # 4 (last value of i)
# Variable defined inside if is accessible outside
if True:
x = 100
print(x) # 100
# But this can be dangerous
if False:
y = 200
# print(y) # Still NameError! y was never assigned because condition was False
Python
# Bad: Using global variable
user_name = “”
def set_name(name):
global user_name
user_name = name
def greet():
print(f”Hello, {user_name}!”)
# Better: Pass parameters and return values
def set_name(name):
return name
def greet(user_name):
print(f”Hello, {user_name}!”)
name = set_name(“Ali”)
greet(name)
# Even better: Use a class (covered in OOP section)
- Trying to modify a global variable without global
- Trying to modify an enclosing variable without nonlocal
- Shadowing built-in names
- Assuming blocks (if, for, while) have their own scope
- Using global excessively when parameters would work better
- Creating circular references with closures that cause memory leaks (advanced)
- What is the difference between local and global scope?
- When do you need to use the global keyword?
- What does LEGB stand for?
- How is nonlocal different from global?
- Can you access a loop variable outside the loop?
- Why is relying on global variables considered bad practice?
⚡ Whisper
Every variable is born somewhere. Some are born in the light of the global scope, visible to all. Others are born in the shadows of a function, hidden and private. Some live for a moment, then vanish when the function ends. Others live forever, clinging to memory in a closure. Know the birthplace of your variables. Know their lifetime. A variable that lives too long becomes a ghost, haunting your code with unexpected values. A variable that dies too soon leaves behind confusion. And a variable that is born in the wrong scope becomes a stranger, unknown to the parts that need it. Scope is not a technicality. It is the geography of your code. It tells you who can see what. It protects you from accidental changes. Learn to read this geography. Place your variables where they belong. Let them live as long as needed, no longer. Then your code will be clear, predictable, and safe.