Python Scope

Master variable visibility and lifetime in Python programs

🔍 What is Scope?

Scope determines where variables can be accessed in your code. Understanding scope is crucial for writing maintainable programs, avoiding naming conflicts, and managing memory efficiently.


# Global scope example
global_var = "I'm global"    # Accessible everywhere

def my_function():
    local_var = "I'm local"  # Only accessible inside function
    print(global_var)        # Can access global
    print(local_var)        # Can access local

# Can access global_var here
print(global_var)

# Can't access local_var here
# print(local_var)  # This would cause an error
                                    
LEGB
Rule
Variable
Visibility
Namespace
Management

Understanding Scope Theory

Python follows the LEGB rule for variable resolution, which defines the order in which Python searches for variables:

🎯 The LEGB Rule

L - Local

Variables defined inside the current function

E - Enclosing

Variables in the local scope of enclosing functions

G - Global

Variables defined at the module level

B - Built-in

Pre-defined names in Python (print, len, etc.)

🧠 Key Principles

  • Variable Lifetime: Variables exist only within their scope
  • Name Resolution: Python searches scopes in LEGB order
  • Assignment Creates Local: Assigning to a variable makes it local by default
  • Reading vs Writing: Different rules apply for reading and modifying variables

Local Scope

Local scope refers to variables defined inside a function. They're only accessible within that function:

Local Scope Examples
# Basic local scope
def greet():
    message = "Hello, World!"  # Local variable
    print(message)

greet()  # Output: Hello, World!

# This would cause an error - message is not accessible outside the function
# print(message)  # NameError: name 'message' is not defined

# Function parameters are also local variables
def add_numbers(a, b):  # a and b are local to this function
    result = a + b      # result is also local
    return result

sum_result = add_numbers(5, 3)
print(sum_result)  # 8

# a, b, and result are not accessible here
# print(a)  # NameError

# Local variables in different functions are independent
def function_one():
    x = 10
    print(f"In function_one: x = {x}")

def function_two():
    x = 20  # Different x, local to function_two
    print(f"In function_two: x = {x}")

function_one()  # In function_one: x = 10
function_two()  # In function_two: x = 20

💡 Local Scope Tips

  • Isolation: Local variables don't interfere with variables in other functions
  • Memory: Local variables are automatically cleaned up when the function ends
  • Parameters: Function parameters are treated as local variables
  • Temporary Storage: Use local variables for temporary calculations

Global Scope

Global variables are defined at the module level and can be accessed from anywhere in the module:

Global Scope Examples
# Global variables
app_name = "My Python App"
version = "1.0.0"
debug_mode = True

def show_app_info():
    # Reading global variables (no special keyword needed)
    print(f"App: {app_name}")
    print(f"Version: {version}")
    print(f"Debug: {debug_mode}")

show_app_info()

# Modifying global variables requires the 'global' keyword
counter = 0  # Global variable

def increment_counter():
    global counter  # Declare that we want to modify the global counter
    counter += 1
    print(f"Counter: {counter}")

def reset_counter():
    global counter
    counter = 0
    print("Counter reset")

increment_counter()  # Counter: 1
increment_counter()  # Counter: 2
reset_counter()      # Counter reset
increment_counter()  # Counter: 1

# Without 'global', assignment creates a local variable
def confusing_function():
    counter = 100  # This creates a NEW local variable
    print(f"Local counter: {counter}")

confusing_function()  # Local counter: 100
print(f"Global counter: {counter}")  # Global counter: 1 (unchanged)

⚠️ Global Variable Warnings

  • Avoid Overuse: Too many global variables make code hard to maintain
  • Use Constants: Global constants (UPPER_CASE) are generally acceptable
  • Thread Safety: Global variables can cause issues in multi-threaded programs
  • Testing: Global state makes unit testing more difficult

Enclosing Scope (Closures)

Enclosing scope occurs when you have nested functions. Inner functions can access variables from outer functions:

Enclosing Scope Examples
# Basic enclosing scope
def outer_function():
    outer_var = "I'm in the outer function"
    
    def inner_function():
        # Can access outer_var from enclosing scope
        print(f"Inner function says: {outer_var}")
    
    inner_function()

outer_function()  # Inner function says: I'm in the outer function

# Closure example - inner function "remembers" outer variables
def create_multiplier(factor):
    def multiply(number):
        return number * factor  # factor is from enclosing scope
    return multiply

# Create specialized functions
double = create_multiplier(2)
triple = create_multiplier(3)

print(double(5))  # 10 (5 * 2)
print(triple(4))  # 12 (4 * 3)

# Modifying enclosing variables with 'nonlocal'
def create_counter():
    count = 0  # Variable in enclosing scope
    
    def increment():
        nonlocal count  # Declare that we want to modify the enclosing variable
        count += 1
        return count
    
    def decrement():
        nonlocal count
        count -= 1
        return count
    
    def get_count():
        return count  # Reading doesn't require nonlocal
    
    return increment, decrement, get_count

# Create counter functions
inc, dec, get = create_counter()

print(inc())  # 1
print(inc())  # 2
print(dec())  # 1
print(get()) # 1

🎯 Closure Benefits

  • Data Encapsulation: Keep related data and functions together
  • Factory Functions: Create specialized functions dynamically
  • State Preservation: Maintain state between function calls
  • Callback Functions: Useful for event handling and decorators

Built-in Scope

Built-in scope contains pre-defined names that are always available in Python:

Built-in Scope Examples
# Built-in functions are always available
print("Hello")  # print is a built-in function
numbers = [1, 2, 3, 4, 5]
print(len(numbers))  # len is built-in
print(max(numbers))  # max is built-in
print(sum(numbers))  # sum is built-in

# View all built-in names
import builtins
print(dir(builtins))  # Shows all built-in names

# You can accidentally shadow built-in names (don't do this!)
def demonstrate_shadowing():
    # This creates a local variable that shadows the built-in 'len'
    len = "I'm not the len function anymore!"
    print(len)  # Prints the string, not the function
    
    # This would cause an error now:
    # print(len([1, 2, 3]))  # TypeError: 'str' object is not callable

demonstrate_shadowing()

# After the function, built-in 'len' is still available
print(len([1, 2, 3]))  # Works fine - prints 3

# Common built-in names you use regularly
data = [1, 2, 3, 4, 5]
print(f"Length: {len(data)}")
print(f"Type: {type(data)}")
print(f"Range: {list(range(5))}")
print(f"Sorted: {sorted(data, reverse=True)}")

# Built-in exceptions
try:
    result = 10 / 0
except ZeroDivisionError:  # ZeroDivisionError is built-in
    print("Cannot divide by zero!")

# Built-in constants
print(f"True: {True}")    # True is built-in
print(f"False: {False}")  # False is built-in
print(f"None: {None}")    # None is built-in

📚 Built-in Categories

Functions

print(), len(), max(), min(), sum(), sorted(), etc.

Types

int, str, list, dict, tuple, set, etc.

Exceptions

ValueError, TypeError, IndexError, etc.

Constants

True, False, None, NotImplemented, etc.

Scope Resolution in Action

Let's see how Python resolves variable names using the LEGB rule:

LEGB Rule Demonstration
# LEGB Rule demonstration
x = "Global x"  # Global scope

def outer():
    x = "Enclosing x"  # Enclosing scope
    
    def inner():
        x = "Local x"  # Local scope
        print(f"Inner function sees: {x}")  # Local x
    
    def inner_no_local():
        print(f"Inner (no local) sees: {x}")  # Enclosing x
    
    inner()
    inner_no_local()
    print(f"Outer function sees: {x}")  # Enclosing x

outer()
print(f"Global scope sees: {x}")  # Global x

# Example with built-in shadowing
def scope_example():
    # This shadows the built-in 'max' function
    max = "I'm not the max function!"
    
    def inner():
        # This creates a local 'max'
        max = 42
        print(f"Local max: {max}")
    
    inner()
    print(f"Enclosing max: {max}")

scope_example()
# Built-in max is still available here
print(f"Built-in max works: {max([1, 5, 3])}")

# Practical example: Configuration system
DEFAULT_CONFIG = {  # Global
    'debug': False,
    'timeout': 30,
    'retries': 3
}

def create_api_client(base_url):
    config = DEFAULT_CONFIG.copy()  # Enclosing scope
    
    def make_request(endpoint, custom_timeout=None):
        # Local scope
        timeout = custom_timeout or config['timeout']  # Uses enclosing config
        debug = config['debug']  # Uses enclosing config
        
        if debug:
            print(f"Making request to {base_url}/{endpoint} with timeout {timeout}")
        
        return f"Response from {base_url}/{endpoint}"
    
    def set_debug(enabled):
        nonlocal config
        config = config.copy()  # Don't modify the original
        config['debug'] = enabled
    
    return make_request, set_debug

# Usage
api_request, set_debug = create_api_client("https://api.example.com")
result = api_request("users")
print(result)

set_debug(True)
result = api_request("posts", custom_timeout=60)
print(result)

Scope Best Practices

✅ Good Practices
  • Minimize Global Variables: Use them sparingly, prefer function parameters
  • Use Constants: Global constants (UPPER_CASE) are acceptable
  • Clear Naming: Use descriptive names to avoid confusion
  • Limit Scope: Keep variables in the smallest scope possible
  • Document Closures: Explain complex closure behavior
❌ Avoid These Mistakes
  • Shadowing Built-ins: Don't use names like 'len', 'max', 'sum'
  • Global Overuse: Too many globals make code hard to test
  • Unclear Modifications: Always use 'global' or 'nonlocal' when modifying
  • Deep Nesting: Avoid too many nested function levels
  • Mutable Defaults: Be careful with mutable default arguments
Best Practices Example
# Good: Use constants for configuration
API_BASE_URL = "https://api.example.com"  # Global constant
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30

# Good: Pass data as parameters instead of using globals
def process_data(data, config):
    """Process data with given configuration."""
    timeout = config.get('timeout', DEFAULT_TIMEOUT)
    # Process with local variables
    return f"Processed {len(data)} items with timeout {timeout}"

# Good: Use closures for factory functions
def create_validator(min_length, max_length):
    """Create a validator function with specific constraints."""
    def validate(text):
        if len(text) < min_length:
            return f"Text too short (minimum {min_length})"
        if len(text) > max_length:
            return f"Text too long (maximum {max_length})"
        return "Valid"
    return validate

# Usage
email_validator = create_validator(5, 50)
password_validator = create_validator(8, 128)

print(email_validator("[email protected]"))  # Valid
print(password_validator("123"))            # Text too short (minimum 8)

# Good: Clear variable names and limited scope
def calculate_total_price(items, tax_rate=0.08):
    """Calculate total price including tax."""
    subtotal = sum(item['price'] for item in items)
    tax_amount = subtotal * tax_rate
    total = subtotal + tax_amount
    
    return {
        'subtotal': subtotal,
        'tax': tax_amount,
        'total': total
    }

# Good: Use classes to manage state instead of global variables
class Counter:
    def __init__(self, initial_value=0):
        self._value = initial_value
    
    def increment(self):
        self._value += 1
        return self._value
    
    def get_value(self):
        return self._value

# Usage
counter = Counter()
print(counter.increment())  # 1
print(counter.increment())  # 2

🧠 Test Your Knowledge

Test your understanding of Python scope:

Question 1: What does the LEGB rule stand for?

Question 2: Which keyword is used to modify a global variable inside a function?

Question 3: What happens when you assign to a variable inside a function without declaring it global?