PyGuide

Learn Python with practical tutorials and code examples

Code Snippet Intermediate
• Updated Aug 5, 2025

Python Variable Scope Functions vs Loops Code Examples

Practical code examples showing why Python variable scope behaves differently inside functions and loops. Solutions for closure issues and late binding problems.

Python Variable Scope Functions vs Loops Code Examples

This collection demonstrates why Python variable scope behaves differently inside functions and loops through practical code examples. Learn how to handle closure issues, late binding problems, and scope resolution with working solutions.

Basic Scope Behavior Comparison #

Function Scope vs Loop Scope #

# Function scope - creates new namespace each time
def function_scope_demo():
    def create_function(value):
        x = value  # New local variable each call
        def inner():
            return f"Function x: {x}"
        return inner
    
    functions = []
    for i in range(3):
        functions.append(create_function(i))
    
    return functions

# Loop scope - shares namespace
def loop_scope_demo():
    functions = []
    for i in range(3):  # Same 'i' variable throughout
        def inner():
            return f"Loop i: {i}"  # Captures by reference
        functions.append(inner)
    
    return functions

# Test both approaches
print("Function scope results:")
func_results = function_scope_demo()
for f in func_results:
    print(f())

print("\nLoop scope results:")
loop_results = loop_scope_demo()
for f in loop_results:
    print(f())

Variable Identity Tracking #

def track_variable_identity():
    """Track how variables are captured in different contexts"""
    
    # Function parameter creates new binding
    def with_parameters():
        functions = []
        
        def make_func(n):  # Parameter creates new scope
            print(f"Creating function with n={n}, id={id(n)}")
            def inner():
                print(f"Executing with n={n}, id={id(n)}")
                return n
            return inner
        
        for i in range(3):
            functions.append(make_func(i))
        
        return functions
    
    # Loop variable shares binding
    def with_loop_variable():
        functions = []
        
        for i in range(3):  # Same variable 'i'
            print(f"Creating function with i={i}, id={id(i)}")
            def inner():
                print(f"Executing with i={i}, id={id(i)}")
                return i
            functions.append(inner)
        
        return functions
    
    print("=== Parameter-based functions ===")
    param_funcs = with_parameters()
    for f in param_funcs:
        f()
    
    print("\n=== Loop variable functions ===")
    loop_funcs = with_loop_variable()
    for f in loop_funcs:
        f()

track_variable_identity()

Late Binding Solutions #

Method 1: Default Arguments #

def late_binding_solutions():
    """Different ways to solve late binding issues"""
    
    # Problem: Late binding
    print("=== Problem: Late Binding ===")
    bad_functions = []
    for i in range(4):
        bad_functions.append(lambda: i * i)
    
    print("Bad functions (all return same value):")
    for f in bad_functions:
        print(f())
    
    # Solution 1: Default arguments
    print("\n=== Solution 1: Default Arguments ===")
    good_functions_1 = []
    for i in range(4):
        good_functions_1.append(lambda x=i: x * x)
    
    print("Good functions with default args:")
    for f in good_functions_1:
        print(f())
    
    # Solution 2: Closure factory
    print("\n=== Solution 2: Closure Factory ===")
    def make_squarer(n):
        return lambda: n * n
    
    good_functions_2 = []
    for i in range(4):
        good_functions_2.append(make_squarer(i))
    
    print("Good functions with factory:")
    for f in good_functions_2:
        print(f())
    
    # Solution 3: Immediate execution
    print("\n=== Solution 3: Immediate Execution ===")
    good_functions_3 = []
    for i in range(4):
        good_functions_3.append((lambda x: lambda: x * x)(i))
    
    print("Good functions with immediate execution:")
    for f in good_functions_3:
        print(f())

late_binding_solutions()

Advanced Closure Patterns #

def advanced_closure_patterns():
    """Advanced patterns for managing closures in loops"""
    
    # Pattern 1: Counter factory with private state
    def create_counter_factory():
        counters = []
        
        for i in range(3):
            def make_counter(start_value):
                count = start_value
                def increment():
                    nonlocal count
                    count += 1
                    return count
                def get_count():
                    return count
                def reset():
                    nonlocal count
                    count = start_value
                    return count
                return increment, get_count, reset
            
            counters.append(make_counter(i * 10))
        
        return counters
    
    print("=== Counter Factory Pattern ===")
    counters = create_counter_factory()
    for i, (inc, get, reset) in enumerate(counters):
        print(f"Counter {i}: start={get()}, inc={inc()}, inc={inc()}, reset={reset()}")
    
    # Pattern 2: Configuration closures
    def create_processors():
        configs = [
            {'operation': 'multiply', 'factor': 2},
            {'operation': 'add', 'value': 10},
            {'operation': 'power', 'exponent': 3}
        ]
        
        processors = []
        for config in configs:
            def make_processor(cfg):  # Parameter captures config
                def process(x):
                    if cfg['operation'] == 'multiply':
                        return x * cfg['factor']
                    elif cfg['operation'] == 'add':
                        return x + cfg['value']
                    elif cfg['operation'] == 'power':
                        return x ** cfg['exponent']
                    return x
                return process
            
            processors.append(make_processor(config))
        
        return processors
    
    print("\n=== Configuration Closure Pattern ===")
    processors = create_processors()
    test_value = 5
    for i, proc in enumerate(processors):
        result = proc(test_value)
        print(f"Processor {i}: {test_value} -> {result}")

advanced_closure_patterns()

Scope Resolution Examples #

LEGB Rule Demonstration #

def legb_rule_examples():
    """Demonstrate LEGB (Local, Enclosing, Global, Built-in) rule"""
    
    # Global variables
    global_var = "I'm global"
    
    def outer_function():
        # Enclosing scope
        enclosing_var = "I'm enclosing"
        
        def inner_function():
            # Local scope
            local_var = "I'm local"
            
            # Accessing variables following LEGB rule
            print(f"Local: {local_var}")
            print(f"Enclosing: {enclosing_var}")
            print(f"Global: {global_var}")
            print(f"Built-in: {len('built-in function')}")  # len is built-in
            
            # Show scope resolution with same names
            def scope_shadowing_demo():
                x = "local x"  # Local
                
                def nested():
                    x = "nested x"  # Shadows enclosing
                    print(f"Nested sees: {x}")
                
                nested()
                print(f"Function sees: {x}")
            
            scope_shadowing_demo()
        
        inner_function()
    
    print("=== LEGB Rule Demonstration ===")
    outer_function()
    
    # Variable resolution in loops
    def loop_scope_resolution():
        functions = []
        x = "enclosing x"
        
        for i in range(3):
            x = f"loop x {i}"  # Modifies enclosing x
            
            def capture_current():
                current_x = x  # Captures current value
                def inner():
                    return current_x
                return inner
            
            def capture_reference():
                def inner():
                    return x  # Captures by reference
                return inner
            
            functions.append(('current', capture_current()))
            functions.append(('reference', capture_reference()))
        
        return functions, x
    
    print("\n=== Loop Scope Resolution ===")
    funcs, final_x = loop_scope_resolution()
    print(f"Final x value: {final_x}")
    
    for i in range(0, len(funcs), 2):  # Process in pairs
        current_func = funcs[i][1]
        ref_func = funcs[i+1][1]
        print(f"Iteration {i//2}: current={current_func()}, reference={ref_func()}")

legb_rule_examples()

Nonlocal and Global in Loops #

def scope_modification_examples():
    """Examples of modifying variables in different scopes from loops"""
    
    # Global modification from loop functions
    counter = 0
    
    def create_global_modifiers():
        functions = []
        
        for i in range(3):
            def make_modifier(increment):
                def modify():
                    global counter
                    counter += increment
                    return counter
                return modify
            
            functions.append(make_modifier(i + 1))
        
        return functions
    
    print("=== Global Variable Modification ===")
    print(f"Initial counter: {counter}")
    
    global_modifiers = create_global_modifiers()
    for i, modifier in enumerate(global_modifiers):
        result = modifier()
        print(f"Modifier {i} result: {result}")
    
    # Nonlocal modification patterns
    def create_enclosing_modifiers():
        shared_state = {'value': 100}
        individual_states = []
        
        for i in range(3):
            individual_state = {'value': i * 10}
            individual_states.append(individual_state)
            
            def make_shared_modifier(idx):
                def modify_shared(delta):
                    nonlocal shared_state
                    shared_state['value'] += delta
                    return f"Modifier {idx}: shared={shared_state['value']}"
                return modify_shared
            
            def make_individual_modifier(state, idx):
                def modify_individual(delta):
                    state['value'] += delta
                    return f"Modifier {idx}: individual={state['value']}"
                return modify_individual
        
        shared_modifiers = [make_shared_modifier(i) for i in range(3)]
        individual_modifiers = [make_individual_modifier(state, i) 
                              for i, state in enumerate(individual_states)]
        
        return shared_modifiers, individual_modifiers
    
    print("\n=== Enclosing Scope Modification ===")
    shared_mods, individual_mods = create_enclosing_modifiers()
    
    print("Shared state modifications:")
    for modifier in shared_mods:
        print(modifier(5))
    
    print("\nIndividual state modifications:")
    for modifier in individual_mods:
        print(modifier(3))

scope_modification_examples()

Mutable Object Behavior #

Shared vs Private Mutable State #

def mutable_object_examples():
    """Examples showing mutable object behavior in closures"""
    
    # Problem: Shared mutable objects
    def shared_mutable_problem():
        shared_list = []
        functions = []
        
        for i in range(3):
            def append_to_shared(value):
                shared_list.append(value)
                return shared_list.copy()  # Return copy to see current state
            
            functions.append(append_to_shared)
        
        return functions
    
    print("=== Shared Mutable Object Problem ===")
    shared_funcs = shared_mutable_problem()
    
    print("Each function modifies the same list:")
    for i, func in enumerate(shared_funcs):
        result = func(f"item_{i}")
        print(f"Function {i} result: {result}")
    
    # Solution: Private mutable objects
    def private_mutable_solution():
        functions = []
        
        for i in range(3):
            def create_private_list():
                private_list = []  # Each closure gets its own list
                
                def append_to_private(value):
                    private_list.append(value)
                    return private_list.copy()
                
                def get_private():
                    return private_list.copy()
                
                def clear_private():
                    private_list.clear()
                    return private_list.copy()
                
                return append_to_private, get_private, clear_private
            
            functions.append(create_private_list())
        
        return functions
    
    print("\n=== Private Mutable Object Solution ===")
    private_funcs = private_mutable_solution()
    
    print("Each function has its own list:")
    for i, (append_func, get_func, clear_func) in enumerate(private_funcs):
        append_func(f"item_{i}")
        append_func(f"item_{i}_2")
        result = get_func()
        print(f"Function {i} list: {result}")
    
    # Advanced: State management with closures
    def create_state_managers():
        managers = []
        
        for i in range(3):
            def create_manager(manager_id):
                state = {
                    'id': manager_id,
                    'data': [],
                    'metadata': {'created': f"manager_{manager_id}", 'operations': 0}
                }
                
                def add_data(item):
                    state['data'].append(item)
                    state['metadata']['operations'] += 1
                    return state.copy()
                
                def get_state():
                    return state.copy()
                
                def reset_state():
                    state['data'].clear()
                    state['metadata']['operations'] = 0
                    return state.copy()
                
                return {
                    'add': add_data,
                    'get': get_state,
                    'reset': reset_state
                }
            
            managers.append(create_manager(i))
        
        return managers
    
    print("\n=== Advanced State Management ===")
    state_managers = create_state_managers()
    
    for i, manager in enumerate(state_managers):
        manager['add'](f"data_{i}_1")
        manager['add'](f"data_{i}_2")
        state = manager['get']()
        print(f"Manager {i}: {state}")

mutable_object_examples()

Debugging and Inspection Tools #

Scope Inspection Utilities #

import sys
import inspect

def scope_debugging_tools():
    """Tools for debugging scope issues in functions and loops"""
    
    def inspect_closure_variables():
        """Inspect what variables are captured by closures"""
        
        functions = []
        debug_info = []
        
        for i in range(3):
            x = i * 10  # Local to loop iteration
            
            def create_inspector(loop_var, local_var):
                def inspector():
                    # Inspect current frame
                    frame = inspect.currentframe()
                    
                    # Get closure variables
                    closure_vars = {}
                    if inspector.__closure__:
                        var_names = inspector.__code__.co_freevars
                        for name, cell in zip(var_names, inspector.__closure__):
                            try:
                                closure_vars[name] = cell.cell_contents
                            except ValueError:
                                closure_vars[name] = "<empty cell>"
                    
                    return {
                        'loop_var': loop_var,
                        'local_var': local_var,
                        'closure_vars': closure_vars,
                        'local_vars': frame.f_locals.copy()
                    }
                
                return inspector
            
            inspector = create_inspector(i, x)
            functions.append(inspector)
            
            # Debug info at creation time
            debug_info.append({
                'creation_time': {'i': i, 'x': x},
                'function': inspector
            })
        
        return functions, debug_info
    
    print("=== Closure Variable Inspection ===")
    inspectors, creation_info = inspect_closure_variables()
    
    for i, (creation, inspector) in enumerate(zip(creation_info, inspectors)):
        print(f"\nInspector {i}:")
        print(f"  Created with: {creation['creation_time']}")
        runtime_info = inspector()
        print(f"  Runtime info: {runtime_info}")
    
    # Variable lifetime tracking
    def track_variable_lifetime():
        """Track how long variables live in different scopes"""
        
        def function_scope_lifetime():
            results = []
            
            for i in range(3):
                def create_tracker(iteration):
                    created_at = f"iteration_{iteration}"
                    
                    def tracker():
                        return f"Created at {created_at}, iteration was {iteration}"
                    
                    return tracker
                
                results.append(create_tracker(i))
            
            return results
        
        def loop_scope_lifetime():
            results = []
            
            for i in range(3):
                created_at = f"iteration_{i}"  # This gets overwritten
                
                def tracker():
                    return f"Created at {created_at}, i is now {i}"
                
                results.append(tracker)
            
            return results
        
        print("\n=== Variable Lifetime Tracking ===")
        
        print("Function scope (each call creates new variables):")
        func_trackers = function_scope_lifetime()
        for j, tracker in enumerate(func_trackers):
            print(f"  Tracker {j}: {tracker()}")
        
        print("\nLoop scope (variables overwritten each iteration):")
        loop_trackers = loop_scope_lifetime()
        for j, tracker in enumerate(loop_trackers):
            print(f"  Tracker {j}: {tracker()}")
    
    track_variable_lifetime()

scope_debugging_tools()

Performance Implications #

Memory Usage Patterns #

import sys
import gc

def performance_analysis():
    """Analyze performance implications of different scope patterns"""
    
    def memory_usage_comparison():
        """Compare memory usage of different closure patterns"""
        
        # Pattern 1: Late binding (memory efficient but incorrect behavior)
        def late_binding_pattern():
            functions = []
            for i in range(1000):
                functions.append(lambda: i)  # All share same variable
            return functions
        
        # Pattern 2: Default arguments (correct behavior, moderate memory)
        def default_arg_pattern():
            functions = []
            for i in range(1000):
                functions.append(lambda x=i: x)  # Each captures its own value
            return functions
        
        # Pattern 3: Factory functions (correct behavior, more memory)
        def factory_pattern():
            functions = []
            for i in range(1000):
                def make_func(n):
                    def inner():
                        return n
                    return inner
                functions.append(make_func(i))
            return functions
        
        print("=== Memory Usage Comparison ===")
        
        # Test each pattern
        patterns = [
            ("Late Binding", late_binding_pattern),
            ("Default Arguments", default_arg_pattern),
            ("Factory Functions", factory_pattern)
        ]
        
        for name, pattern_func in patterns:
            # Force garbage collection before measurement
            gc.collect()
            
            # Create functions
            functions = pattern_func()
            
            # Measure approximate memory usage
            total_size = sum(sys.getsizeof(f) for f in functions)
            avg_size = total_size / len(functions)
            
            print(f"{name}:")
            print(f"  Total size: {total_size} bytes")
            print(f"  Average per function: {avg_size:.2f} bytes")
            print(f"  First function result: {functions[0]()}")
            print(f"  Last function result: {functions[-1]()}")
            print()
            
            # Clean up
            del functions
            gc.collect()
    
    memory_usage_comparison()
    
    # Performance timing
    import time
    
    def timing_comparison():
        """Compare execution timing of different patterns"""
        
        def time_pattern(name, create_func, iterations=10000):
            # Time function creation
            start_time = time.time()
            functions = create_func()
            creation_time = time.time() - start_time
            
            # Time function execution
            start_time = time.time()
            for _ in range(iterations):
                for f in functions[:100]:  # Test first 100 functions
                    f()
            execution_time = time.time() - start_time
            
            print(f"{name}:")
            print(f"  Creation time: {creation_time:.6f} seconds")
            print(f"  Execution time: {execution_time:.6f} seconds ({iterations * 100} calls)")
            print()
            
            return functions
        
        print("=== Performance Timing Comparison ===")
        
        # Test patterns
        late_binding = lambda: [lambda: i for i in range(100)]
        default_args = lambda: [lambda x=i: x for i in range(100)]
        factory = lambda: [(lambda n: lambda: n)(i) for i in range(100)]
        
        time_pattern("Late Binding", late_binding)
        time_pattern("Default Arguments", default_args)
        time_pattern("Factory Functions", factory)
    
    timing_comparison()

performance_analysis()

Real-World Applications #

Event Handler Registration #

def event_handler_examples():
    """Real-world examples: Event handler registration patterns"""
    
    class EventSystem:
        def __init__(self):
            self.handlers = {}
        
        def register(self, event_type, handler):
            if event_type not in self.handlers:
                self.handlers[event_type] = []
            self.handlers[event_type].append(handler)
        
        def trigger(self, event_type, data=None):
            if event_type in self.handlers:
                results = []
                for handler in self.handlers[event_type]:
                    results.append(handler(data))
                return results
            return []
    
    # Problem: Incorrect handler registration
    def register_handlers_wrong(event_system):
        handlers = ['click', 'hover', 'focus']
        
        for handler_name in handlers:
            # Wrong: all handlers will use the last value of handler_name
            event_system.register(handler_name, 
                                lambda data: f"Wrong {handler_name} handler: {data}")
    
    # Solution: Correct handler registration
    def register_handlers_correct(event_system):
        handlers = ['click', 'hover', 'focus']
        
        for handler_name in handlers:
            # Correct: capture current value with default argument
            event_system.register(handler_name,
                                lambda data, name=handler_name: f"Correct {name} handler: {data}")
    
    # Alternative: Factory function approach
    def register_handlers_factory(event_system):
        handlers = ['click', 'hover', 'focus']
        
        def create_handler(name):
            def handler(data):
                return f"Factory {name} handler: {data}"
            return handler
        
        for handler_name in handlers:
            event_system.register(handler_name, create_handler(handler_name))
    
    print("=== Event Handler Registration ===")
    
    # Test wrong approach
    wrong_system = EventSystem()
    register_handlers_wrong(wrong_system)
    
    print("Wrong approach results:")
    for event in ['click', 'hover', 'focus']:
        results = wrong_system.trigger(event, f"{event}_data")
        print(f"  {event}: {results}")
    
    # Test correct approach
    correct_system = EventSystem()
    register_handlers_correct(correct_system)
    
    print("\nCorrect approach results:")
    for event in ['click', 'hover', 'focus']:
        results = correct_system.trigger(event, f"{event}_data")
        print(f"  {event}: {results}")
    
    # Test factory approach
    factory_system = EventSystem()
    register_handlers_factory(factory_system)
    
    print("\nFactory approach results:")
    for event in ['click', 'hover', 'focus']:
        results = factory_system.trigger(event, f"{event}_data")
        print(f"  {event}: {results}")

event_handler_examples()

Dynamic Function Generation #

def dynamic_function_examples():
    """Examples of dynamically generating functions with proper scope handling"""
    
    def create_validators():
        """Create form field validators dynamically"""
        
        validation_rules = [
            {'field': 'email', 'pattern': r'.*@.*', 'message': 'Invalid email'},
            {'field': 'phone', 'pattern': r'\d{10}', 'message': 'Invalid phone'},
            {'field': 'zip', 'pattern': r'\d{5}', 'message': 'Invalid zip code'}
        ]
        
        validators = {}
        
        # Wrong way: late binding issue
        wrong_validators = {}
        for rule in validation_rules:
            import re
            wrong_validators[rule['field']] = lambda value: (
                re.match(rule['pattern'], value) is not None,
                rule['message']  # This will always use the last rule's message!
            )
        
        # Correct way: capture rule with factory function
        for rule in validation_rules:
            def create_validator(validation_rule):
                import re
                compiled_pattern = re.compile(validation_rule['pattern'])
                
                def validator(value):
                    is_valid = compiled_pattern.match(str(value)) is not None
                    return is_valid, validation_rule['message']
                
                return validator
            
            validators[rule['field']] = create_validator(rule)
        
        return wrong_validators, validators
    
    print("=== Dynamic Validator Generation ===")
    
    wrong_validators, correct_validators = create_validators()
    
    test_data = {
        'email': 'test@example.com',
        'phone': '1234567890',
        'zip': '12345'
    }
    
    print("Wrong validators (all use same message):")
    for field, value in test_data.items():
        is_valid, message = wrong_validators[field](value)
        print(f"  {field}: valid={is_valid}, message='{message}'")
    
    print("\nCorrect validators:")
    for field, value in test_data.items():
        is_valid, message = correct_validators[field](value)
        print(f"  {field}: valid={is_valid}, message='{message}'")
    
    # Advanced: Creating configurable processors
    def create_data_processors():
        """Create data processing functions with different configurations"""
        
        processor_configs = [
            {'name': 'normalize', 'func': lambda x: x / 100, 'description': 'Normalize to 0-1'},
            {'name': 'square', 'func': lambda x: x ** 2, 'description': 'Square the value'},
            {'name': 'logarithm', 'func': lambda x: __import__('math').log(x) if x > 0 else 0, 'description': 'Natural log'}
        ]
        
        processors = {}
        
        for config in processor_configs:
            def create_processor(cfg):
                def processor(data):
                    if isinstance(data, (list, tuple)):
                        return [cfg['func'](item) for item in data]
                    else:
                        return cfg['func'](data)
                
                # Add metadata to the function
                processor.name = cfg['name']
                processor.description = cfg['description']
                
                return processor
            
            processors[config['name']] = create_processor(config)
        
        return processors
    
    print("\n=== Dynamic Data Processors ===")
    
    processors = create_data_processors()
    test_data = [1, 4, 9, 16, 25]
    
    for name, processor in processors.items():
        try:
            result = processor(test_data)
            print(f"{processor.name} ({processor.description}): {result}")
        except Exception as e:
            print(f"{processor.name}: Error - {e}")

dynamic_function_examples()

Summary #

These examples demonstrate the key differences in how Python handles variable scope inside functions versus loops:

Core Concepts:

  • Functions create new scope contexts, loops share existing scope
  • Closures capture variables by reference, leading to late binding issues
  • Different solutions exist for proper variable capture in loops

Common Patterns:

  • Use default arguments to capture current values
  • Factory functions for clean closure creation
  • Proper state management with nonlocal and global
  • Debugging techniques for scope issues

Best Practices:

  • Prefer explicit parameter passing over closure capture
  • Use factory functions for complex closure scenarios
  • Consider performance implications of different patterns
  • Test closure behavior thoroughly in loops

Related Topics:

Related Snippets

Snippet Beginner

Python Code Examples: Handling Functions That Return Nothing

Ready-to-use code examples for working with Python functions that return None, including detection, handling, and best practices.

#python #return #none +2
View Code
Syntax
Snippet Intermediate

Python Error Frame Inspection Code Examples

Ready-to-use Python code snippets for inspecting error frames, analyzing stack traces, and debugging frame-related issues effectively.

#python #error #frame +2
View Code
Syntax
Snippet Intermediate

Python Import Module Not Found Error Solutions Django Flask Code Examples

Ready-to-use Python code examples for fixing import module not found errors in Django and Flask. Practical snippets for common web framework import issues.

#python #import-error #django +4
View Code
Web Development
Snippet Intermediate

Python Mixed Whitespace Debugging Tools and Code Examples

Ready-to-use Python functions and tools for detecting and fixing mixed tabs and spaces indentation errors in your code files.

#python #debugging #whitespace +3
View Code
Syntax