Course topics

By WebNest Studio

Python Tutorial

Scope, Closures and Namespaces

When Python sees a name like total, which variable does it mean — one inside the function, one in an enclosing function, a module-level one, or a built-in? The answer follows the LEGB rule. Understanding scope explains the global and nonlocal keywords, closures (functions that remember variables from where they were created), and the famous UnboundLocalError.

Namespaces and the LEGB Rule

A namespace maps names to objects. Python looks names up in four scopes, in order: Local (inside the current function), Enclosing (outer functions), Global (module level) and Built-in (len, print...). locals() and globals() return the current namespaces as dicts.

global and nonlocal

Assigning to a name inside a function makes it local to that function for the whole function body. To assign to a module-level variable, declare global name; to assign to a variable of an enclosing function, declare nonlocal name. Global state makes code hard to test — prefer passing values in and returning results.

Closures

A closure is an inner function that remembers variables from its enclosing scope even after the outer function has returned. Closures create function factories (make_multiplier(3)), stateful counters without classes, and are the mechanism behind decorators.

Examples

The LEGB lookup order

Python
x = "global"

def outer():
    x = "enclosing"
    def inner():
        x = "local"
        print("inner sees:", x)
    inner()
    print("outer sees:", x)

outer()
print("module sees:", x)
print("built-in len:", len("abc"))
Output
inner sees: local
outer sees: enclosing
module sees: global
built-in len: 3

UnboundLocalError, global and nonlocal

Python
counter = 0

def broken():
    counter += 1          # assignment makes counter local -> error

def increment():
    global counter
    counter += 1

try:
    broken()
except UnboundLocalError as e:
    print("UnboundLocalError:", e)

increment(); increment()
print("counter:", counter)

def make_counter():
    count = 0
    def next_value():
        nonlocal count
        count += 1
        return count
    return next_value

tick = make_counter()
print(tick(), tick(), tick())
Output
UnboundLocalError: cannot access local variable 'counter' where it is not associated with a value
counter: 2
1 2 3

Closures remember their environment

Python
def make_discount(percent):
    def apply(price):
        return round(price * (1 - percent / 100), 2)
    return apply

festival = make_discount(20)
student = make_discount(50)
print(festival(1000), student(1000))
print(festival.__closure__[0].cell_contents)

def running_average():
    values = []
    def add(v):
        values.append(v)
        return sum(values) / len(values)
    return add

avg = running_average()
print(avg(10), avg(20), avg(60))
Output
800.0 500.0
20
10.0 15.0 30.0

Common Mistakes

  • Reading a global and then assigning to it in the same function without "global", causing UnboundLocalError.
  • Using global variables for state that should be passed as arguments or stored in objects.
  • Forgetting nonlocal when updating an enclosing variable from an inner function.
  • Shadowing built-ins (list, max, id) in a scope and breaking later code.

Key Points to Remember

  • Names resolve in LEGB order: Local, Enclosing, Global, Built-in.
  • Assignment inside a function creates a local unless declared global or nonlocal.
  • Closures capture enclosing variables and keep them alive.
  • Closures enable function factories, stateful callables and decorators.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.