Skip to main content

Python Variable Scope, Closures, and the LEGB Rule

Intermediate25 min5 exercises70 XP
0/5 exercises

Imagine a house with several rooms. Each room has its own set of objects — a lamp on the bedroom nightstand, a knife in the kitchen drawer. You can use items in the room you're standing in, and you can walk to the hallway to grab something shared. But you can't reach into someone else's closed room without opening the door first.

Python variables work the same way. Every variable lives in a scope — a specific region of your code where that variable exists and can be used. Understanding scope is the key to writing clean functions that don't accidentally overwrite each other's data.

In this tutorial, you'll learn the four scope levels in Python (the LEGB rule), how to use the global and nonlocal keywords, and how closures let inner functions remember values from their parent. By the end, you'll be able to debug the most confusing "variable not found" errors with confidence.

What Is Local Scope?

When you create a variable inside a function, it only exists inside that function. This is called local scope. Think of it as a private room — nothing outside the function can see or touch it.

Local variables live inside the function
Loading editor...

The variable message is created inside greet(), so it's local to that function. Once greet() finishes, message is gone. If you try to use it outside, Python raises a NameError.

Each function call creates a fresh set of local variables. They don't carry over between calls:

Local variables reset every call
Loading editor...

What Is Global Scope?

A variable created outside any function lives in the global scope. It's like an item in the hallway of your house — every room can see it.

Global variables are visible inside functions
Loading editor...

Both functions can read the global variable language. But what happens if you try to change it inside a function?

You cannot modify a global variable directly
Loading editor...

This crashes with an UnboundLocalError. Python sees counter = ... inside the function and assumes counter is a local variable. But then the right side tries to read counter before it has been assigned locally. Python gets confused.

The LEGB Rule: How Python Finds Variables

When you use a variable name, Python searches four places in a specific order. This is called the LEGB rule:

  • L — Local: The current function's variables.
  • E — Enclosing: Variables in any outer (parent) function (for nested functions).
  • G — Global: Variables at the top level of the module (the file).
  • B — Built-in: Python's built-in names like print, len, range.
  • Python checks these in order: L first, then E, then G, then B. It stops at the first match. If it finds nothing in any of them, you get a NameError.

    LEGB rule in action
    Loading editor...

    Each print(x) finds a different x because it searches from its own scope outward. The inner function finds its local x first and stops looking.

    The global Keyword

    If you need to modify a global variable from inside a function, you must tell Python explicitly with the global keyword. This is like getting a special key to reach into the hallway and move things around.

    Using the global keyword
    Loading editor...

    The global score declaration tells Python: "I'm not creating a new local variable. I want to use the global one." Now the assignment score = score + points works correctly.

    Using global (harder to trace)
    total = 0
    
    def add(n):
        global total
        total = total + n
    
    add(5)
    add(3)
    print(total)  # 8
    Using return (cleaner)
    def add(current, n):
        return current + n
    
    total = 0
    total = add(total, 5)
    total = add(total, 3)
    print(total)  # 8

    Enclosing Scope and Closures

    When you define a function inside another function, the inner function can access variables from the outer function. This outer function's scope is called the enclosing scope — the "E" in LEGB.

    A closure remembers the enclosing variable
    Loading editor...

    Something remarkable happens here. make_greeting("Hello") finishes running and returns the inner greet function. Normally, the local variable greeting would be destroyed. But the inner function still needs it!

    Python keeps greeting alive because greet references it. This combination of a function plus the variables it remembers from its enclosing scope is called a closure.

    Closures are useful for creating specialized functions. Here's a practical example — a multiplier factory:

    Closure as a function factory
    Loading editor...

    The nonlocal Keyword

    Just like global lets you modify global variables, nonlocal lets you modify variables in the enclosing scope. Without it, assigning to the variable would create a new local one instead.

    Using nonlocal to modify enclosing variables
    Loading editor...

    Without nonlocal count, the line count = count + 1 would crash with an UnboundLocalError — the same problem we saw with globals. The nonlocal keyword tells Python to look one scope outward for the variable.

    This pattern is powerful. The count variable is completely hidden from the outside world. No code outside make_counter can access or tamper with it. This is a form of encapsulation — keeping data private.

    Common Scope Mistakes and How to Fix Them

    Here are the scope bugs that trip up Python developers most often. Knowing these patterns will save you hours of debugging.

    Mistake 1: Shadowing a Global Variable

    Shadowing: local hides the global
    Loading editor...

    The items = [] inside the function creates a brand new local variable that shadows (hides) the global one. The global items list is never modified. This is usually a bug.

    Mistake 2: UnboundLocalError from Mixing Read and Write

    UnboundLocalError explained
    Loading editor...

    Python scans the entire function body before running it. If it sees any assignment to total, it marks total as local for the whole function. Then the print(total) line tries to read a local variable that hasn't been assigned yet.


    Practice Exercises

    Time to put scope and closures into practice. These exercises progress from reading scope to building your own closures.

    Exercise 1: Predict the Scope Output
    Predict Output

    Read this code carefully and predict what it prints. Then write a single print() statement that outputs the exact result.

    x = 10
    
    def foo():
        x = 20
        print(x)
    
    foo()
    print(x)

    Print the two numbers on separate lines, exactly as the code above would output them.

    Loading editor...
    Exercise 2: Fix the Global Counter Bug
    Fix the Bug

    This code tries to increment a global counter, but it crashes with an UnboundLocalError. Fix it so the output is:

    1
    2
    3
    Loading editor...
    Exercise 3: Build a Greeting Closure
    Write Code

    Write a function called make_greeter that takes a greeting string as an argument and returns a new function. The returned function should take a name argument and print the greeting with the name.

    For example:

    hello = make_greeter("Hello")
    hello("World")

    Should print: Hello, World!

    The test will call:

    hi = make_greeter("Hi")
    hi("Alice")
    hi("Bob")

    Expected output:

    Hi, Alice!
    Hi, Bob!
    Loading editor...
    Exercise 4: Build a Counter with nonlocal
    Write Code

    Write a function called make_counter that takes a start value and returns a function. Each time the returned function is called, it should print the current count and then increment it by 1.

    The test will call:

    c = make_counter(1)
    c()
    c()
    c()

    Expected output:

    1
    2
    3
    Loading editor...
    Exercise 5: Build a Running Average Closure
    Write Code

    Write a function called make_averager that returns a function. Each time the returned function is called with a number, it should print the running average of all numbers seen so far.

    The running average is the sum of all numbers divided by the count.

    The test will call:

    avg = make_averager()
    avg(10)
    avg(20)
    avg(30)

    Expected output:

    10.0
    15.0
    20.0

    Hint: the averager needs to remember both the total sum and the count of numbers.

    Loading editor...

    Summary

    Here's what you've learned about Python scope:
    ------
    Local scopeVariables created inside a function, exist only there
    Global scopeVariables created at the module level, visible everywhere
    LEGB rulePython searches Local, Enclosing, Global, Built-in — in that order
    global keywordLets a function modify a module-level variable
    Enclosing scopeAn outer function's variables, accessible by inner functions
    ClosureA function that remembers variables from its enclosing scope
    nonlocal keywordLets an inner function modify an enclosing variable

    The golden rule: prefer passing arguments and returning values over using global or nonlocal. Closures are the one exception where enclosing scope shines — they let you create specialized functions with private state.

    What's Next?

    Now that you understand scope and closures, you're ready for [Recursion](/python/python-recursion) — where functions call themselves. Understanding scope is essential for recursion because each recursive call gets its own local scope.