Python Scope
Learn about variable scope in Python and how Python resolves variable names using the LEGB rule.
Python Scope
A variable is only available from inside the region it is created. This is called scope.
Local Scope
A variable created inside a function belongs to the local scope of that function, and can only be used inside that function.
Example - A variable created inside a function is available inside that function:
def myfunc():
x = 300
print(x)
myfunc()Function Inside Function
As explained in the example above, the variable x is not available outside the function, but it is available for any function inside the function:
Example - The local variable can be accessed from a function within the function:
def myfunc():
x = 300
def myinnerfunc():
print(x)
myinnerfunc()
myfunc()Global Scope
A variable created in the main body of the Python code is a global variable and belongs to the global scope.
Global variables are available from within any scope, global and local.
Example - A variable created outside of a function is global and can be used by anyone:
x = 300
def myfunc():
print(x)
myfunc()
print(x)Naming Variables
If you operate with the same variable name inside and outside of a function, Python will treat them as two separate variables, one available in the global scope (outside the function) and one available in the local scope (inside the function):
Example - The function will print the local x, and then the code will print the global x:
x = 300
def myfunc():
x = 200
print(x)
myfunc()
print(x)Global Keyword
If you need to create a global variable, but are stuck in the local scope, you can use the global keyword.
The global keyword makes the variable global.
Example - If you use the global keyword, the variable belongs to the global scope:
def myfunc():
global x
x = 300
myfunc()
print(x)Also, use the global keyword if you want to make a change to a global variable inside a function.
Example - To change the value of a global variable inside a function, refer to the variable by using the global keyword:
x = 300
def myfunc():
global x
x = 200
myfunc()
print(x)Nonlocal Keyword
The nonlocal keyword is used to work with variables inside nested functions.
The nonlocal keyword makes the variable belong to the outer function.
Example - If you use the nonlocal keyword, the variable will belong to the outer function:
def myfunc1():
x = "Jane"
def myfunc2():
nonlocal x
x = "hello"
myfunc2()
return x
print(myfunc1())LEGB Rule
Python follows the LEGB rule to resolve variable names:
- L - Local (inside the current function)
- E - Enclosing (in any outer function)
- G - Global (at the module level)
- B - Built-in (in the built-in namespace)
Example - LEGB rule demonstration:
# Built-in scope (B)
# print, len, str, etc. are built-in functions
# Global scope (G)
x = "global x"
def outer_function():
# Enclosing scope (E)
x = "enclosing x"
def inner_function():
# Local scope (L)
x = "local x"
print(f"Inner function: {x}") # Local x
def inner_function2():
print(f"Inner function 2: {x}") # Enclosing x
inner_function()
inner_function2()
print(f"Outer function: {x}") # Enclosing x
outer_function()
print(f"Global: {x}") # Global xScope Examples
Example 1: Variable Shadowing
name = "Global Alice"
def greet():
name = "Local Bob" # This shadows the global variable
print(f"Hello, {name}!")
def greet_global():
print(f"Hello, {name}!") # Uses global variable
greet() # Hello, Local Bob!
greet_global() # Hello, Global Alice!
print(name) # Global AliceExample 2: Modifying Global Variables
counter = 0
def increment():
global counter
counter += 1
print(f"Counter: {counter}")
def increment_local():
counter = 10 # Creates a new local variable
counter += 1
print(f"Local counter: {counter}")
print(f"Initial counter: {counter}") # 0
increment() # Counter: 1
increment() # Counter: 2
increment_local() # Local counter: 11
print(f"Final counter: {counter}") # 2Example 3: Nested Functions and Nonlocal
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
def decrement():
nonlocal count
count -= 1
return count
def get_count():
return count
return increment, decrement, get_count
# Create counter functions
inc, dec, get = make_counter()
print(get()) # 0
print(inc()) # 1
print(inc()) # 2
print(dec()) # 1
print(get()) # 1Class Scope
Classes have their own scope rules. Class variables are shared among all instances:
Example - Class and instance variables:
class MyClass:
class_var = "I'm a class variable"
def __init__(self, name):
self.instance_var = name # Instance variable
def show_vars(self):
print(f"Class variable: {MyClass.class_var}")
print(f"Instance variable: {self.instance_var}")
def modify_class_var(self):
MyClass.class_var = "Modified class variable"
@classmethod
def class_method(cls):
print(f"Accessing class variable from class method: {cls.class_var}")
@staticmethod
def static_method():
print("Static method - no access to class or instance variables")
# Usage
obj1 = MyClass("Object 1")
obj2 = MyClass("Object 2")
obj1.show_vars()
obj2.show_vars()
print("\nAfter modifying class variable:")
obj1.modify_class_var()
obj1.show_vars()
obj2.show_vars() # Both objects see the change
MyClass.class_method()
MyClass.static_method()Scope and Loops
In Python, loop variables have function scope, not block scope:
Example - Loop variable scope:
def demonstrate_loop_scope():
# Loop variables persist after the loop
for i in range(3):
x = i * 2
print(f"i after loop: {i}") # i is still accessible
print(f"x after loop: {x}") # x is still accessible
# List comprehension has its own scope
squares = [y**2 for y in range(5)]
# print(y) # This would cause an error - y is not accessible
return squares
result = demonstrate_loop_scope()
print(f"Squares: {result}")Practical Examples
Example 1: Configuration Manager
class ConfigManager:
_instance = None
_config = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def set_config(self, key, value):
ConfigManager._config[key] = value
def get_config(self, key, default=None):
return ConfigManager._config.get(key, default)
def show_all_config(self):
for key, value in ConfigManager._config.items():
print(f"{key}: {value}")
# Global configuration
config = ConfigManager()
def setup_database():
# Function scope - modifying global config
config.set_config("db_host", "localhost")
config.set_config("db_port", 5432)
def set_credentials():
# Nested function scope - still modifying global config
config.set_config("db_user", "admin")
config.set_config("db_password", "secret")
set_credentials()
def setup_api():
config.set_config("api_key", "abc123")
config.set_config("api_url", "https://api.example.com")
# Setup configuration
setup_database()
setup_api()
print("Final configuration:")
config.show_all_config()Example 2: Closure Example
def create_multiplier(factor):
"""Creates a function that multiplies by a given factor"""
def multiplier(number):
# This function "closes over" the factor variable
return number * factor
return multiplier
def create_accumulator(initial_value=0):
"""Creates a function that accumulates values"""
total = initial_value
def accumulate(value):
nonlocal total
total += value
return total
def get_total():
return total
def reset():
nonlocal total
total = initial_value
# Return multiple functions that share the same scope
accumulate.get_total = get_total
accumulate.reset = reset
return accumulate
# Usage
double = create_multiplier(2)
triple = create_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
# Accumulator example
acc = create_accumulator(10)
print(acc(5)) # 15
print(acc(3)) # 18
print(acc.get_total()) # 18
acc.reset()
print(acc.get_total()) # 10Common Scope Pitfalls
Pitfall 1: Late Binding Closures
# Problem: All functions refer to the same variable
functions = []
for i in range(3):
functions.append(lambda: i) # All lambdas refer to the same 'i'
# All functions return 2 (the final value of i)
for func in functions:
print(func()) # 2, 2, 2
# Solution 1: Use default parameter
functions_fixed1 = []
for i in range(3):
functions_fixed1.append(lambda x=i: x)
for func in functions_fixed1:
print(func()) # 0, 1, 2
# Solution 2: Use closure
def make_func(n):
return lambda: n
functions_fixed2 = []
for i in range(3):
functions_fixed2.append(make_func(i))
for func in functions_fixed2:
print(func()) # 0, 1, 2Pitfall 2: Mutable Default Arguments
# Problem: Mutable default argument
def add_item(item, target_list=[]): # Don't do this!
target_list.append(item)
return target_list
# The same list is reused across calls
list1 = add_item("first")
list2 = add_item("second")
print(list1) # ['first', 'second'] - unexpected!
print(list2) # ['first', 'second'] - same list!
# Solution: Use None as default
def add_item_fixed(item, target_list=None):
if target_list is None:
target_list = []
target_list.append(item)
return target_list
list3 = add_item_fixed("first")
list4 = add_item_fixed("second")
print(list3) # ['first']
print(list4) # ['second']Best Practices
- Use descriptive variable names to avoid confusion
- Minimize use of global variables
- Use
globalandnonlocalkeywords explicitly when needed - Be careful with mutable default arguments
- Understand closure behavior with loops
- Keep functions small and focused to reduce scope complexity
- Use class methods and static methods appropriately
The LEGB Rule
Python resolves a name by searching four scopes in order: Local → Enclosing → Global → Built-in. The first match wins.
| Scope | Where |
|---|---|
| Local | Inside the current function |
| Enclosing | In an outer (nested) function |
| Global | At the top level of the module |
| Built-in | print, len, … always available |
global and nonlocal
count = 0
def increment():
global count # rebind the module-level variable
count += 1
increment()
print(count) # 1
def outer():
x = "outer"
def inner():
nonlocal x # rebind the enclosing variable
x = "changed"
inner()
return x
print(outer()) # changedReassigning a global inside a function without declaring global creates a new local variable instead — a common source of bugs. Prefer returning values over mutating globals.
Try It Yourself
Exercise 1: Predict the output.
x = 10
def f():
x = 20
print(x)
f()
print(x)Show solution
Prints 20 then 10. Inside f, x = 20 creates a new local variable; the global x is untouched.
Exercise 2: Fix a counter function so it increments a module-level total.
Show solution
total = 0
def add():
global total
total += 1
add(); add()
print(total) # 2Key Takeaways
- Names resolve by LEGB: Local, Enclosing, Global, Built-in.
- Use
global/nonlocalto rebind outer variables (sparingly). - Assigning inside a function makes a local unless declared otherwise.
📘 Real-World Deep Dive
Python's variable-resolution chain is "LEGB" (Local, Enclosing, Global, Builtins) and the <code>global</code>/<code>nonlocal</code> statements gate mutation. Misunderstanding this is the source of countless silent bugs.
Real-Life Scenario
A counter-as-closure used as a memoisation decorator, and a logging counter threaded through an inline function with <code>nonlocal</code>.
Real-Life Example
from typing import Any
# 1) Simple counter closure with 'nonlocal' for mutation.
def make_counter():
n = 0
def incr() -> int:
nonlocal n # rebind outer n
n += 1
return n
return incr
ticks = make_counter()
print("tick:", ticks(), ticks(), ticks())
# 2) A safety wrapper that records how many times a function runs.
def count_calls(fn):
n = 0
def wrapper(*args, **kw):
nonlocal n
n += 1
print(f"{fn.__name__} call #{n}")
return fn(*args, **kw)
return wrapper
@count_calls
def greet(name: str) -> str:
return f"hello, {name}"
for who in ["Ada", "Bo", "Cy"]:
greet(who)Expected Output
tick: 1 2 3
greet call #1
greet call #2
greet call #3Common mistakes
- Globals are not shared between threads implicitly; rely on
threading.localinstead. - Comprehensions create their own scope; a variable defined inside doesn't leak out.
nonlocalrequires a real enclosing scope — at module level,nonlocalis aSyntaxError.
🚀 Performance & Best Practices
- Local lookups are faster than global — bind hot globals to a local inside the function.
- Closures over many free variables slow attribute access measurably; keep captures small.
- Use classes instead of closures for stateful objects with multiple fields.
🧪 Try It Yourself
- Replace
count_callswith a class decorator that holds count + args. - Add a
reset_counthelper that usesnonlocalto zero the counter. - Write a
cached_propertyexample usingnonlocalfor the cache.