Web Analytics

Variable Scope

Intermediate ~25 min read

Understanding scope is essential for writing bug-free Python code. Scope determines where variables can be accessed and modified. Python follows the LEGB rule - Local, Enclosing, Global, Built-in - to look up variable names. Master scope and you'll avoid one of the most common sources of confusion in programming!

Local Scope

Variables created inside a function are local to that function. They're created when the function is called and destroyed when it returns. Local variables cannot be accessed from outside the function, and each function call gets fresh local variables.

Output
Click Run to execute your code
Why Local Scope Exists: Local scope protects you from accidentally modifying variables elsewhere in your program. When a function has its own private variables, you can understand and test it in isolation. This is a key principle of good software design called encapsulation.

Global Scope and the global Keyword

Variables defined at the module level (outside any function) are global. Functions can read global variables freely, but to modify them, you must use the global keyword. However, overusing global variables is generally considered bad practice.

Output
Click Run to execute your code
Avoid Global Variables: Global state makes code harder to understand, test, and debug. Functions that modify globals have "hidden" dependencies. Instead, pass values as arguments and return results. Use globals only for true constants (like PI = 3.14159) or configuration that genuinely needs to be module-wide.

The LEGB Rule

When Python encounters a variable name, it searches for it in a specific order: Local, Enclosing, Global, Built-in. This is the LEGB rule. Understanding it helps you predict which variable Python will find when names overlap.

Output
Click Run to execute your code
LEGB Memory Aid: Local (inside current function) → Enclosing (outer function if nested) → Global (module level) → Built-in (Python's built-in names like len, print). Python stops at the first match it finds.

The nonlocal Keyword

When you have nested functions and need to modify a variable from an enclosing (but not global) scope, use nonlocal. This is especially useful for creating closures - functions that "remember" state from their enclosing scope.

Output
Click Run to execute your code

Common Mistakes

1. UnboundLocalError when modifying globals

count = 0

def increment():
    count += 1  # UnboundLocalError!
    # Python sees assignment, creates local 'count'
    # But tries to read count before it's assigned

# Fix: use global keyword
def increment():
    global count
    count += 1

2. Shadowing built-in names

# Bad - shadows built-in list()
list = [1, 2, 3]
# Later...
new_list = list("hello")  # TypeError: 'list' object is not callable

# Fix: use different variable names
items = [1, 2, 3]
new_list = list("hello")  # Works: ['h', 'e', 'l', 'l', 'o']

3. Thinking assignment creates reference to global

data = [1, 2, 3]

def modify():
    data = [4, 5, 6]  # Creates LOCAL data!
    # Global data is unchanged

modify()
print(data)  # [1, 2, 3] - surprised?

# If you want to modify the global:
def modify():
    global data
    data = [4, 5, 6]

4. Using global when nonlocal is needed

def outer():
    x = 10

    def inner():
        global x  # Wrong! Looks for module-level x
        x += 1

    inner()
    print(x)  # Still 10 - inner modified a different x

# Fix: use nonlocal for enclosing scope
def outer():
    x = 10

    def inner():
        nonlocal x  # Correct!
        x += 1

    inner()
    print(x)  # 11

5. Mutable globals can be modified without 'global'

# Surprising: this works without 'global'
items = []

def add_item(item):
    items.append(item)  # Modifying, not reassigning!

add_item("apple")
print(items)  # ['apple']

# But this needs 'global':
def reset_items():
    global items
    items = []  # Reassignment needs global

Exercise: Counter Factory

Task: Create a function that returns a counter function using closures.

Requirements:

  • Create make_counter(start=0)
  • Returns a function that increments and returns the count
  • Use nonlocal to maintain state
Output
Click Run to execute your code
Show Solution
def make_counter(start=0):
    """Create a counter function starting at 'start'."""
    count = start

    def counter():
        nonlocal count
        count += 1
        return count

    return counter

# Test it
counter1 = make_counter()
counter2 = make_counter(100)

print(f"Counter 1: {counter1()}, {counter1()}, {counter1()}")
print(f"Counter 2: {counter2()}, {counter2()}")

Summary

  • Local: Variables inside functions - created on call, destroyed on return
  • Global: Variables at module level - use global keyword to modify
  • LEGB: Lookup order: Local → Enclosing → Global → Built-in
  • nonlocal: Modify enclosing (not global) scope in nested functions
  • Shadowing: Local variables can shadow global/built-in names
  • Best practice: Avoid global state; prefer passing values and returning results
  • Closures: Functions that capture and remember enclosing scope

What's Next?

Now that you understand scope, let's learn about Lambda Functions - small, anonymous functions perfect for short operations. Lambdas are widely used with functions like map(), filter(), and sorted()!