Functions And Functional Basics
Scope & Closures
local_var = "I only exist inside this function"
Jr Codex Python Notes
Level: Intermediate Prerequisites: Chapter 2 Time to complete: ~25 minutes
Table of Contents
- What is Scope?
- The LEGB Rule
- The
globalKeyword - Nested Functions &
nonlocal - Closures
- Why Closures Matter
- 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 definedVariables 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 scopeEach 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 wins3. 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) # 2Best 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 needed4. 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| Keyword | Used For |
|---|---|
global | Modify a variable in the module's global scope from inside a function |
nonlocal | Modify 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=2Closure 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) # 26. 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)) # False7. 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
globalornonlocalto 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
- What are the four scopes in the LEGB rule, and in what order does Python search them?
- Why does calling
increment()withoutglobal countraise anUnboundLocalError? - In
make_multiplier, why doesdouble"remember"factor=2aftermake_multiplierhas already returned?
Next Chapter
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index