Python Scope & LEGB Rule
In Python, the scope of a variable determines where in your program that name is visible and accessible. When you reference a variable name, Python searches four nested namespaces in a strict sequence known as the LEGB Rule:
Python stops searching as soon as it finds the first matching variable name in the LEGB hierarchy. If the name is not found in any of the four scopes, a NameError is raised.
# Demonstrating the LEGB scope hierarchy:
global_var = "๐ I am GLOBAL"
def outer_function():
enclosing_var = "๐ฆ I am ENCLOSING (Outer Function)"
def inner_function():
local_var = "๐ I am LOCAL (Inner Function)"
print("Inside inner function:")
print(" 1.", local_var) # Local Scope (L)
print(" 2.", enclosing_var) # Enclosing Scope (E)
print(" 3.", global_var) # Global Scope (G)
print(" 4. Built-in len:", len("Test")) # Built-in Scope (B)
inner_function()
outer_function()
inner_function accesses its own local variable (L), its parent's enclosing variable (E), module-level global variable (G), and built-in function len() (B) smoothly.
By default, if you assign to a variable inside a function (x = 100), Python creates a brand new Local variable, even if a global variable named x already exists! To rebind a global variable from inside a function, declare it with global:
counter = 0 # Global variable
def increment_bad():
# Attempting counter += 1 without 'global' raises UnboundLocalError!
# Because assignment makes 'counter' local before reading it!
pass
def increment_safe():
global counter # Explicitly declares intent to rebind the global variable
counter += 1
print("Global counter incremented to:", counter)
increment_safe()
increment_safe()
print("Final Global Counter:", counter)
Avoid overusing global variables in production code because global state makes programs difficult to debug and test in parallel.
When a nested inner function references variables from its enclosing function, it forms a Closure. To rebind an enclosing variable from the inner function, use the nonlocal keyword:
def create_bank_account(initial_balance):
balance = initial_balance # Enclosing variable
def deposit(amount):
nonlocal balance # Rebinds enclosing balance!
balance += amount
return f"Deposited Rs.{amount} | Current Balance: Rs.{balance}"
return deposit
# Create an isolated account closure:
my_account = create_bank_account(1000)
print(my_account(500))
print(my_account(250))
balance remains alive in memory attached to my_account, providing private state encapsulation without writing a full class!
A Pure Function is a function that:
- Given the same arguments, ALWAYS returns the exact same result (Deterministic).
- Produces zero side effects (does not mutate global state, modify passed lists in-place, or write to external files).
# โ IMPURE FUNCTION (Mutates external global state):
total_sales = 0
def add_sale_impure(amount):
global total_sales
total_sales += amount
return total_sales
# โ
PURE FUNCTION (Deterministic, zero side-effects):
def calculate_sale_pure(current_total, new_amount):
return current_total + new_amount
print("Pure Result 1:", calculate_sale_pure(100, 50)) # 150
print("Pure Result 2:", calculate_sale_pure(100, 50)) # Always 150!
Pure functions are effortless to unit test, cache (memoization), and execute concurrently across multiple CPU threads.
A pure, modular converter engine supporting currency exchange and metric units:
# Dynamic Currency & Unit Converter Engine
# Currency Rates relative to 1 USD (Base)
EXCHANGE_RATES = {
"USD": 1.00,
"INR": 86.50,
"EUR": 0.92,
"GBP": 0.79,
"JPY": 152.00
}
def convert_currency(amount, from_curr, to_curr):
"""Convert currency using pure conversion arithmetic."""
from_rate = EXCHANGE_RATES.get(from_curr.upper())
to_rate = EXCHANGE_RATES.get(to_curr.upper())
if not from_rate or not to_rate:
return None, "โ Invalid Currency Code!"
# Convert from source to USD base, then to target currency
amount_in_usd = amount / from_rate
converted_amount = amount_in_usd * to_rate
return round(converted_amount, 2), f"1 {from_curr} = {to_rate/from_rate:.4f} {to_curr}"
# Test Currency Conversions:
amt_inr, rate_info = convert_currency(100, "USD", "INR")
print(f"๐ฒ 100 USD = Rs.{amt_inr} INR ({rate_info})")
amt_eur, rate_info2 = convert_currency(5000, "INR", "EUR")
print(f"๐ถ 5000 INR = โฌ{amt_eur} EUR ({rate_info2})")
By converting first to a common base (USD), we can convert between any arbitrary pair of currencies with just $N$ stored exchange rates instead of $N^2$ pairs!
If you read a global variable and then assign to it in the same function without declaring "global x", Python flags the variable as local across the ENTIRE function body, raising UnboundLocalError when reading it on earlier lines.
Create a unit converter function convert_distance(value, from_unit, to_unit) supporting "km", "miles", and "meters".
def convert_distance(val, from_unit, to_unit):
to_meters = {"km": 1000, "meters": 1, "miles": 1609.34}
meters = val * to_meters[from_unit]
result = meters / to_meters[to_unit]
return round(result, 2)
print("5 km in miles:", convert_distance(5, "km", "miles"), "miles")
print("10 miles in km:", convert_distance(10, "miles", "km"), "km")
Q What is the LEGB rule in Python?
LEGB stands for Local, Enclosing, Global, Built-in. It defines the exact 4-step hierarchy Python uses to resolve variable names.
Q What is the difference between global and nonlocal?
global binds a variable name to the top-level module scope. nonlocal binds a variable name to the nearest enclosing parent function scope in nested functions.
Q Why are pure functions preferred in modern software development?
Pure functions produce no side effects and always return identical outputs for identical inputs, making them deterministic, bug-resistant, and easy to test and parallelize.