Skip to content

Closures and Scope (LEGB) in Python

Mastering Closures and Scope in Python: A Comprehensive Guide

Every variable in Python lives in a scope — a region of the program where that name is visible. Understanding scope explains why some variables are accessible inside a function and others are not, and why changing a variable inside a function sometimes affects the outside and sometimes does not. Closures build on scope: they let an inner function remember variables from the function that created it. In this guide, we’ll explore the LEGB rule, the globalglobal and nonlocalnonlocal keywords, and closures.

What is Scope?

Scope determines where a name (variable, function) can be accessed. Python resolves names using the LEGB rule, searching four scopes in order:

  1. Local — names assigned inside the current function.
  2. Enclosing — names in any enclosing (outer) function.
  3. Global — names at the top level of the module.
  4. Built-in — names pre-defined by Python (lenlen, printprint, rangerange, …).

Diagram:

diagram LEGB rule mermaid
Order Python searches scopes to resolve a name

The following example shows all four levels being resolved:

legb.py
x = "global"          # Global scope
 
def outer():
    y = "enclosing"   # Enclosing scope
 
    def inner():
        z = "local"   # Local scope
        print(z)      # found in Local
        print(y)      # found in Enclosing
        print(x)      # found in Global
        print(len)    # found in Built-in
 
    inner()
 
outer()
legb.py
x = "global"          # Global scope
 
def outer():
    y = "enclosing"   # Enclosing scope
 
    def inner():
        z = "local"   # Local scope
        print(z)      # found in Local
        print(y)      # found in Enclosing
        print(x)      # found in Global
        print(len)    # found in Built-in
 
    inner()
 
outer()

Output:

command
C:\Users\Your Name> python legb.py
local
enclosing
global
<built-in function len>
command
C:\Users\Your Name> python legb.py
local
enclosing
global
<built-in function len>

Local Scope

A variable assigned inside a function is local to that function and disappears when the function returns. It cannot be accessed from outside.

local.py
def greet():
    message = "Hello"   # local variable
    print(message)
 
greet()
print(message)          # NameError: 'message' is not defined
local.py
def greet():
    message = "Hello"   # local variable
    print(message)
 
greet()
print(message)          # NameError: 'message' is not defined

Output:

command
C:\Users\Your Name> python local.py
Hello
Traceback (most recent call last):
NameError: name 'message' is not defined
command
C:\Users\Your Name> python local.py
Hello
Traceback (most recent call last):
NameError: name 'message' is not defined

Global Scope and the global Keyword

By default, a function can read a global variable but assigning to a name inside a function creates a new local variable. To modify a global variable from inside a function, use the globalglobal keyword.

global_keyword.py
count = 0
 
def increment_wrong():
    count = count + 1     # ERROR: count is treated as local
 
def increment_right():
    global count          # tell Python to use the global 'count'
    count = count + 1
 
increment_right()
increment_right()
print(count)
global_keyword.py
count = 0
 
def increment_wrong():
    count = count + 1     # ERROR: count is treated as local
 
def increment_right():
    global count          # tell Python to use the global 'count'
    count = count + 1
 
increment_right()
increment_right()
print(count)

Output:

command
C:\Users\Your Name> python global_keyword.py
2
command
C:\Users\Your Name> python global_keyword.py
2

Enclosing Scope and the nonlocal Keyword

When you have a function defined inside another function, the inner function can read the outer function’s variables. To reassign an enclosing variable, use the nonlocalnonlocal keyword.

nonlocal_keyword.py
def counter():
    count = 0
 
    def increment():
        nonlocal count     # refer to 'count' in the enclosing scope
        count += 1
        return count
 
    print(increment())
    print(increment())
    print(increment())
 
counter()
nonlocal_keyword.py
def counter():
    count = 0
 
    def increment():
        nonlocal count     # refer to 'count' in the enclosing scope
        count += 1
        return count
 
    print(increment())
    print(increment())
    print(increment())
 
counter()

Output:

command
C:\Users\Your Name> python nonlocal_keyword.py
1
2
3
command
C:\Users\Your Name> python nonlocal_keyword.py
1
2
3

What is a Closure?

A closure is a function that remembers the variables from its enclosing scope even after that outer function has finished running. Three conditions make a closure:

  1. There is a nested (inner) function.
  2. The inner function refers to a variable from the enclosing function.
  3. The enclosing function returns the inner function.
closure.py
def make_multiplier(factor):
    # 'factor' is captured by the inner function
    def multiplier(number):
        return number * factor
    return multiplier        # return the function, do not call it
 
double = make_multiplier(2)
triple = make_multiplier(3)
 
print(double(5))   # 10
print(triple(5))   # 15
closure.py
def make_multiplier(factor):
    # 'factor' is captured by the inner function
    def multiplier(number):
        return number * factor
    return multiplier        # return the function, do not call it
 
double = make_multiplier(2)
triple = make_multiplier(3)
 
print(double(5))   # 10
print(triple(5))   # 15

Output:

command
C:\Users\Your Name> python closure.py
10
15
command
C:\Users\Your Name> python closure.py
10
15

Even though make_multipliermake_multiplier has already returned, doubledouble still remembers factor = 2factor = 2 and tripletriple still remembers factor = 3factor = 3. Each closure carries its own captured value.

Diagram:

diagram closure mermaid
How a closure captures an enclosing variable

Inspecting a Closure

You can see the values a closure has captured through its __closure____closure__ attribute.

inspect_closure.py
double = make_multiplier(2)
print(double.__closure__[0].cell_contents)
inspect_closure.py
double = make_multiplier(2)
print(double.__closure__[0].cell_contents)

Output:

command
C:\Users\Your Name> python inspect_closure.py
2
command
C:\Users\Your Name> python inspect_closure.py
2

Why Closures Matter

  • They let you build function factories (like make_multipliermake_multiplier) that generate customised functions.
  • They are the foundation of decorators.
  • They keep data private inside a function without needing a class.

Conclusion

Scope controls where names are visible, and Python resolves them with the LEGB rule: Local, then Enclosing, then Global, then Built-in. Use globalglobal to reassign a module-level variable and nonlocalnonlocal to reassign an enclosing function’s variable. Closures take this further — an inner function returned from an outer one remembers the variables it captured, enabling powerful patterns like function factories and decorators. For more hands-on examples and in-depth tutorials, explore our resources on Python Central Hub!


Try it: Closures and Scope Exercises

Exercise 1 – The global Keyword

Exercise 2 – The nonlocal Keyword

Exercise 3 – Build a Closure

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did