Python

Functions And Functional Basics

Scope & Closures

local_var = "I only exist inside this function"

JrCodex·5 min read

Jr Codex Python Notes

Level: Intermediate Prerequisites: Chapter 2 Time to complete: ~25 minutes


Table of Contents

  1. What is Scope?
  2. The LEGB Rule
  3. The global Keyword
  4. Nested Functions & nonlocal
  5. Closures
  6. Why Closures Matter
  7. Summary & Next Steps

1. What is Scope?

Scope determines where in your code a variable name is visible and accessible.

def my_function():
    local_var = "I only exist inside this function"
    print(local_var)
 
my_function()
print(local_var)     # NameError: name 'local_var' is not defined

Variables created inside a function are local to it — they don't leak out, and they disappear once the function returns.


2. The LEGB Rule

When Python looks up a variable name, it searches four scopes in this order:

LEGB — Search Order
─────────────────────────────────────────
  L — Local     : inside the current function
  E — Enclosing  : inside any enclosing function (for nested functions)
  G — Global      : at the top level of the module/file
  B — Built-in     : Python's built-in names (len, print, range...)
─────────────────────────────────────────
x = "global"
 
def outer():
    x = "enclosing"
 
    def inner():
        x = "local"
        print(x)       # "local" — found in Local scope first
 
    inner()
    print(x)             # "enclosing" — found in outer's Local scope
 
outer()
print(x)                   # "global" — found in module's Global scope

Each level shadows the ones after it — Python stops at the first match it finds.

print(len)          # <built-in function len> — found in Built-in scope
 
def len(x):           # shadows the built-in! (don't actually do this)
    return "surprise"
 
print(len([1,2,3]))    # "surprise" — your local def now wins

3. The global Keyword

By default, assigning to a variable inside a function creates a new local variable, even if a global variable with the same name exists:

count = 0
 
def increment():
    count = count + 1    # UnboundLocalError! Python sees the assignment
                           # and treats `count` as local for the WHOLE function
increment()

To actually modify the global variable, declare it explicit with global:

count = 0
 
def increment():
    global count
    count = count + 1
 
increment()
increment()
print(count)      # 2

Best practice: avoid global where possible — it makes functions harder to reason about (side effects, ordering dependencies). Prefer passing values in and returning results out.

# Prefer this:
def increment(count):
    return count + 1
 
count = 0
count = increment(count)
count = increment(count)
print(count)          # 2 — no global needed

4. Nested Functions & nonlocal

The same "assignment creates a local variable" rule applies to nested functions and their enclosing scope — nonlocal is the fix there:

def make_counter():
    count = 0
 
    def increment():
        nonlocal count    # without this, count += 1 raises UnboundLocalError
        count += 1
        return count
 
    return increment
 
counter = make_counter()
print(counter())      # 1
print(counter())       # 2
print(counter())        # 3
KeywordUsed For
globalModify a variable in the module's global scope from inside a function
nonlocalModify a variable in an enclosing function's scope from inside a nested function

5. Closures

A closure is a nested function that "remembers" variables from its enclosing scope, even after the outer function has finished running. The make_counter() example above is already a closure — let's look at why it works.

def make_multiplier(factor):
    def multiply(number):
        return number * factor    # `factor` is remembered from the enclosing scope
    return multiply
 
double = make_multiplier(2)
triple = make_multiplier(3)
 
print(double(5))      # 10
print(triple(5))       # 15
print(double(10))       # 20 — `double` still remembers factor=2
Closure Mechanics
─────────────────────────────────────────
  make_multiplier(2) runs, creates `multiply`, RETURNS `multiply`.
  Normally `factor` would disappear once make_multiplier() returns...
  ...but `multiply` keeps a reference to it — that's the closure.

  double = multiply function + remembered factor=2
  triple = multiply function + remembered factor=3
  (Two SEPARATE closures, each with its own captured `factor`.)
─────────────────────────────────────────

You can inspect what a closure has captured:

print(double.__closure__[0].cell_contents)    # 2

6. Why Closures Matter

Closures aren't just a curiosity — they're the mechanism behind several patterns you'll meet later:

  • Decorators (Module 5) are closures that wrap another function.
  • Callback functions that need to "remember" configuration without global state.
  • Data hiding — a lightweight alternative to a full class when you just need one function with private state.
def make_validator(min_value, max_value):
    """Returns a validator function that remembers its bounds."""
    def validate(value):
        return min_value <= value <= max_value
    return validate
 
is_valid_age = make_validator(0, 120)
is_valid_percentage = make_validator(0, 100)
 
print(is_valid_age(25))            # True
print(is_valid_age(150))            # False
print(is_valid_percentage(105))      # False

7. Summary & Next Steps

Key Takeaways

  • Scope determines variable visibility; Python resolves names using the LEGB order: Local → Enclosing → Global → Built-in.
  • Assigning to a name inside a function makes it local by default — use global or nonlocal to explicitly modify an outer-scope variable (but prefer passing values in/out instead).
  • A closure is a nested function that captures and remembers variables from its enclosing scope after that scope has finished executing.
  • Closures are the foundation for decorators (Module 5) and are a lightweight way to bundle a function with private, remembered state.

Concept Check

  1. What are the four scopes in the LEGB rule, and in what order does Python search them?
  2. Why does calling increment() without global count raise an UnboundLocalError?
  3. In make_multiplier, why does double "remember" factor=2 after make_multiplier has already returned?

Next Chapter

Chapter 4: Recursion


Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index