Q1What does this code print?stock = 100
def f():
stock = 50
f()
print(stock)
Inner Functions and Closures — Mastering Scope with global and nonlocal
Walk through Python scope, inner functions, and closures, then return a function that remembers outer values — all in your browser.
In the previous article you saw how arguments and return values behave. This time you'll dig into a few more function-related topics: how Python keeps variables inside and outside a function separate (scope), how to define functions inside functions (inner functions), and how to return a function that remembers outer values (a closure). Along the way, you'll see when to reach for global and nonlocal to rewrite an enclosing variable from inside a function.
Global and Local Variables — Scopes Are Separate
Variables defined outside any function are global variables; variables defined inside a function are local variables. You can read a global from inside a function, but if you assign to the same name with = value inside the function, Python creates a new local variable — a separate thing from the outer one.
If you check a variable's memory address with id(), you'll see the outer and inner ones point to different locations.
stock = 100 # global variable
def show_stock():
print(f"inside: {stock}") # 100 — reads the outer value
def try_change():
stock = 50 # creates a brand-new local variable
print(f"inside: {stock}") # 50
show_stock() # inside: 100
try_change() # inside: 50
print(f"outside: {stock}") # outside: 100 ← outer is unchanged
- stock = 100 — global variable
- Functions can only read it from inside
print(stock)→ reads the outer 100- Creates no local variable
stock = 50— creates a new local inside the function- Doesn't affect the outer
stock
Rewriting an Outer Variable with the global Keyword
When you actually want to rewrite an outer global from inside a function, declare global variable_name at the top of the function. That tells Python, "this is that outer global, not a new local." Without it, an assignment like count += 1 mixes a read and a write of the same name and you get UnboundLocalError ("local variable referenced before assignment").
visit_count = 0
# × Without global → UnboundLocalError
# def increment():
# visit_count += 1
# increment() ← UnboundLocalError
# ○ global lets you rewrite the outer global
def increment():
global visit_count
visit_count += 1
increment()
increment()
increment()
print(visit_count) # 3
Keep global to a Minimum
global lets a function silently rewrite outer state, which makes a single bad write 100 lines away surprisingly hard to track down at scale.
Default to returning a value with return and assigning it on the caller side, and reach for classes (covered later) when you really need stateful behavior.
How Functions and Variables Live in Memory — Names vs. Things
Internally, Python keeps a namespace — a "name → thing" lookup. x = 5 says "create the integer 5 in memory and make the name x point to it."
def f(): ... works the same way: it builds a function object (the function body) in memory and makes the name f point to it. There's exactly one of these lookups per program — the global namespace — set up when the module loads and kept around until the program exits.
- x = 5 — name
xpoints to the integer 5 - def f — name
fpoints to a function object - Stays alive until the program exits
- Holds arguments and locals
- Tossed entirely on return
- Completely separate from the first
- Locals are independent
def once and the function is registered in the global namespace, where it stays until exit. Each call, however, builds its own local frame and discards it on return. The inner functions, closures, and nonlocal covered next all build on this frame structure.Inner Functions — Defining Functions Inside Functions
Stack another def inside a function and you get an inner function — one that only exists for use inside the outer function. They're a great way to name a meaningful chunk of logic inside a long function so the body reads as a clean sequence of steps.
Since an inner function isn't reachable from outside, it's also a good fit for hiding helpers you don't want to expose.
- Calling
validatefrom out here → NameError - Doesn't exist outside
- Holds arguments
name/age - Can call
validatefrom inside
- Reads the outer
name/age - Not exposed to the outside
def process_user(name, age):
def validate():
if not name or not isinstance(age, int) or age < 0:
raise ValueError("Invalid input")
validate() # call the inner function
print(f"Processed {name} ({age})")
process_user("Alice", 25)
# Processed Alice (25)
# validate isn't reachable from outside process_user
# validate() ← NameError
Great When You Want to Split a Long Function
Once a function grows past 50 or 100 lines, splitting it into inner functions named for each meaningful chunk — process_name() / process_age() and so on — turns the outer function into a readable list of steps. If you ever want to reuse one outside, promoting an inner function to a regular function is trivial.
Closures — Functions That Remember Outer Values
Inner functions can read the outer function's arguments and locals. Take it one step further — have the outer function return the inner function — and you've built a function that keeps working with the outer values baked in. That's a closure.
It's a clean way to mass-produce similar functions that differ only in a setting, like "a function that triples" and "a function that quintuples". Take the setting (factor) as an outer argument and reference it from the inner function: make_multiplier(3) returns "a multiply that remembers 3", and make_multiplier(5) returns "a multiply that remembers 5".
def make_multiplier(factor):
def multiply(x):
return x * factor # references the outer factor
return multiply
times3 = make_multiplier(3) # function that remembers factor=3
times5 = make_multiplier(5) # a separate function that remembers factor=5
print(times3(10)) # 30
print(times5(10)) # 50
print(times3(7)) # 21
Closures Hand Out "Pre-Configured" Functions
When you want to create lots of similar calculations that differ only in a setting — "10% tax" vs "8% tax" and so on — closures shine. Instead of passing the setting on every call, you hand out a single pre-configured function, and the calling code stays much cleaner.
nonlocal — Rewriting an Enclosing Function's Variable
Closures can read the outer variable just fine, but as with global, trying to rewrite it via count += 1 blows up with UnboundLocalError. The keyword that allows that rewrite is nonlocal. Where global targets the module level, nonlocal targets the immediately enclosing function's local.
Wrap State Inside a Function Object
nonlocal is the canonical way to wrap a counter that grows on every call inside a function.
Unlike global, the state stays sealed inside a specific function object, so side effects don't spread, and you get safer state than going global.
def create_counter():
x = 0
def increment():
nonlocal x # declares we're updating the x from create_counter
x += 1
return x
return increment
counter = create_counter()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
# A second counter has its own independent x
counter2 = create_counter()
print(counter2()) # 1 — unrelated to counter
counter = create_counter()— receivesincrement- Outside code can't touch
xdirectly
- x = 0 — counter, created exactly once
- What
incrementreferences vianonlocal
nonlocal x— points at the outerxx += 1updates the outer x
Knowledge Check
Answer each question one by one.
Q2Why does this code error out? Pick the best explanation.count = 0
def inc():
count += 1
inc()
Q3What does print(times3(10)) print?def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
times3 = make_multiplier(3)