Learn by reading through in order

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
Inside vs. Outside Are Separate Scopes — A Set-Diagram View
Module (Global Namespace)
  • stock = 100 — global variable
  • Functions can only read it from inside
show_stock()'s Local Namespace
  • print(stock) → reads the outer 100
  • Creates no local variable
try_change()'s Local Namespace
  • stock = 50 — creates a new local inside the function
  • Doesn't affect the outer stock
Local namespaces nest inside the module. Reads reach out, but assignments stay sealed in as separate locals.

Use a global stock variable to confirm that reads reach out, but assignments inside don't.

① Declare a global stock = 100.

② Define def show_stock(): and print f"inside: {stock}".

③ Define def try_change(): and assign stock = 50, then print f"inside: {stock}".

④ Call show_stock()try_change()print(f"outside: {stock}") and check that the outer value hasn't changed.

(When the answer is correct, the explanation will appear.)

Python Editor

Run code to see output

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").

global Declares "That Outer Variable"
no globalcount += 1UnboundLocalErrorglobal countcount += 1rewrites theouter countfailssucceeds
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.

Build a counter for page visits using global.

① Declare visit_count = 0 outside any function.

② Define def increment_visit():. At the top of the function, write global visit_count, then visit_count += 1.

③ Call increment_visit() three times, then print f"visits: {visit_count}".

Python Editor

Run code to see output

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.

Module and Function Frames in Memory
Global Namespace (Module)
  • x = 5 — name x points to the integer 5
  • def f — name f points to a function object
  • Stays alive until the program exits
f()'s First Call Frame
  • Holds arguments and locals
  • Tossed entirely on return
f()'s Second Call Frame
  • Completely separate from the first
  • Locals are independent
Each function call gets a fresh local namespace (frame) in memory, then it disappears on exit. There's only one global namespace for the whole program.
How def and Function Calls Use Memory
①Run`def f(): ...`Register the functionin the global nsStays in the global nsuntil program exits②First call: f()Create a fresh localnamespace (frame)Discard the frameon `return`③Second call: f()Build a brand-new frame(independent of #1)Discard the frameon `return`at run timecallon exitcall againon exit
Run 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.

Inner Function Scope — A Set-Diagram View
Module (Global Namespace)
  • Calling validate from out here → NameError
  • Doesn't exist outside
process_user()'s Frame
  • Holds arguments name / age
  • Can call validate from inside
validate()'s Frame
  • Reads the outer name / age
  • Not exposed to the outside
validate only lives inside process_user. It can read the outer arguments, and the outside world never sees it.
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 chunkprocess_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.

Reshape an order-processing function with a dedicated inner function for input checks.

① Define def process_order(item, quantity):.

② Inside it, define def validate():. Inside that, raise ValueError("Invalid order") if item is empty, quantity isn't an int, or quantity is 0 or below.

③ Call validate(), then print f"Order received: {quantity} x {item}".

④ Call process_order("apple", 3) and check the result.

Python Editor

Run code to see output

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
How a Closure Works — Definition and Call Flow
①Callmake_multiplier(3)②Outer framebuilt with factor=3④return multiply(remembers factor=3)③def multiplyrefers to factor inside⑤times3 = make_multiplier(3)⑥times3(10)→ 10 × 3(factor) = 30rundefine insidereturn the remembering fnreceivecall
make_multiplier's outer frame disappears on return, but the multiply built inside walks out remembering factor=3.

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.

Build a function that returns a discount-applying function that remembers the discount rate.

① Define def make_discounter(rate):. Inside it, define def apply(price): that returns int(price * (1 - rate)). Don't forget to return apply.

② Build two functions: discount_10 = make_discounter(0.1) and discount_30 = make_discounter(0.3).

③ Print discount_10(1000) and discount_30(1000), and check that different discount rates apply to the same price.

Python Editor

Run code to see output

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
nonlocal Updates the Enclosing Function's Variable — A Set-Diagram View
Module (Global Namespace)
  • counter = create_counter() — receives increment
  • Outside code can't touch x directly
create_counter()'s Frame
  • x = 0 — counter, created exactly once
  • What increment references via nonlocal
increment()'s Frame
  • nonlocal x — points at the outer x
  • x += 1 updates the outer x
nonlocal targets the immediately enclosing function's variable. State lives inside the function object, not the global namespace.

Build something like an ID generator function that issues order IDs starting from 1, using a closure with nonlocal.

① Define def create_order_id_issuer(): and initialize next_id = 1 at the top.

② Inside it, define def issue():. After nonlocal next_id, write current = next_idnext_id += 1return current.

③ Finally return issue to hand the inner function out.

④ Get it via issue_id = create_order_id_issuer() and call print(issue_id()) three times — confirm you see 1 → 2 → 3.

Python Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1What does this code print?
stock = 100
def f():
stock = 50
f()
print(stock)

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)