Closures & Lexical Scope

Reviewed & published by Brayan K

Once you master closures, you unlock the ability to create custom function factories, stateful functions, decorators, event handlers, configuration-based logic, and real-world abstractions used in production systems.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

Once you master closures, you unlock the ability to create:

This lesson takes you from theory → real project engineering.

🔥 1. The Core Idea: Lexical Scope

🏠 Real-World Analogy:

Think of lexical scope like a house with rooms. Each room (function) can see into the hallway (outer scope), but the hallway can't see into the rooms. Inner functions can "see" outer variables, but not vice versa.

TermWhat It Means
Lexical ScopeVariable visibility is determined by where code is written, not where it runs
Inner FunctionCan see variables from outer function ✅
Outer FunctionCannot see variables from inner function ❌
x = 10  # Global scope

def outer():
    y = 20  # outer() scope
    
    def inner():
        # inner() can see BOTH x and y!
        print(f"x = {x}, y = {y}")
    
    return inner  # Return the inner function

func = outer()  # outer() finishes, but...
func()  # inner() STILL remembers y! → prints "x = 10, y = 20"

# ✅ Expected output:
# x = 10, y = 20

🔒 2. What Exactly Is a Closure?

🎒 The Backpack Analogy:

A closure is like a backpack that a function carries. When you create an inner function, it "packs" any variables it needs from the outer function. Even after the outer function is done, the inner function still has its backpack with all those values!

StepWhat Happens
1. Nested functionA function is defined inside another function
2. Captures variablesThe inner function uses variables from the outer function
3. Returned/passed outThe outer function returns the inner function
4. Remembers!The inner function retains access to those captured variables forever
def make_multiplier(factor):
    # 'factor' will be "packed" into the closure
    def multiply(x):
        return x * factor  # Uses 'factor' from outer scope
    return multiply

times_10 = make_multiplier(10)  # factor=10 is captured
times_5 = make_multiplier(5)    # factor=5 is captured separately

print(times_10(3))  # 30 (uses its own factor=10)
print(times_5(3))   # 15 (uses its own factor=5)

# You can actually SEE the closure!
print(times_10.__closure__)  # Shows the cell objects
print(times_10.__closure__[0].cell_contents)  # Shows: 10

🧠 3. Why Closures Matter in Real Projects

Closures solve real engineering problems:

⚡ 4. Real Project Example #1 — A Counter Without Classes

⚠️ The nonlocal Keyword

When you want to modify (not just read) an outer variable, you MUST use nonlocal. Without it, Python thinks you're creating a NEW local variable!

ActionNeeds nonlocal?
Reading outer variable: print(count)No ✅
Modifying outer variable: count += 1Yes! 🔑
def counter():
    count = 0  # This variable lives in the closure
    
    def increment():
        nonlocal count  # "I want to MODIFY the outer 'count'"
        count += 1
        return count
    
    return increment

# Create two SEPARATE counters
counter_a = counter()
counter_b = counter()

print(counter_a())  # 1 - counter_a's count
print(counter_a())  # 2 - counter_a's count
print(counter_b())  # 1 - counter_b has its OWN count!
print(counter_a())  # 3 - counter_a continues

# ✅ Expected output:
# 1
# 2
# 1
# 3

💡 Why This Matters: Each counter has its own private count. This is used for tracking events, API call limits, unique ID generators, and session tracking.

📖 Worked Example: A Bank Account Made of Closures

Sections 1, 2 and 4 each showed one piece. This puts them together into something you would genuinely ship: an account object built entirely from closures, with private state that cannot be reached from outside.

The new idea here is that several inner functions can share the same captured variable. deposit, withdraw and statement are three separate functions, but there is exactly one balance between them. Read every comment before you run it.

"""A bank account with no class in sight — just closures sharing one variable."""

def open_account(owner, opening_balance=0.0):
    balance = opening_balance        # lives in the closure, not in a global

    def deposit(amount):
        nonlocal balance             # "modify the OUTER balance, don't make a new one"
        if amount <= 0:
            raise ValueError("deposit must be positive")
        balance += amount
        return balance

    def withdraw(amount):
        nonlocal balance
        if amount > balance:
            raise ValueError(f"{owner} has only {balance:.2f}")   # owner is captured too
        balance -= amount
        return balance

    def statement():
        # No nonlocal here — READING an outer variable never needs it.
        return f"{owner}: {balance:.2f}"

    # All three share ONE balance, because they close over the same variable.
    return deposit, withdraw, statement


deposit, withdraw, statement = open_account("Ada", 100.00)

print(statement())
print(deposit(50.00))       # 100 + 50
print(withdraw(30.00))      # 150 - 30
print(statement())

# A second account gets its OWN backpack — nothing is shared between the two.
d2, w2, s2 = open_account("Grace")
d2(10.00)
print(s2())
print(statement())          # Ada is untouched by anything Grace does

try:
    withdraw(1000.00)       # the rule lives inside the closure, so it always applies
except ValueError as e:
    print("Refused:", e)

# Proof: the free variables each function carries in its backpack.
print("deposit carries:", sorted(deposit.__code__.co_freevars))
print("statement carries:", sorted(statement.__code__.co_freevars))

# ✅ Expected output:
# Ada: 100.00
# 150.0
# 120.0
# Ada: 120.00
# Grace: 10.00
# Ada: 120.00
# Refused: Ada has only 120.00
# deposit carries: ['balance']
# statement carries: ['balance', 'owner']

The last two lines print __code__.co_freevars — the list of outer variables each function packed into its backpack. deposit only needs balance; statement needs both balance and owner. Python works this out when it compiles the function, not when it runs.

🎯 Your Turn: A Scoreboard That Remembers

Same shape as the account, smaller. Everything is written for you except the three things this lesson is about: the keyword for modifying a captured variable, reading a captured variable, and returning the inner functions so the outside world can use them.

# 🎯 YOUR TURN — replace the three ___ blanks

def make_scoreboard(team):
    score = 0                    # this lives in the closure, one per scoreboard

    def add(points):
        ___ score                # 👉 the keyword that lets you MODIFY an outer variable
        score += points
        return score

    def show():
        # Reading needs no keyword at all — only modifying does.
        return f"{___}: {score}"  # 👉 which captured variable holds the team name?

    return add, ___              # 👉 hand BOTH inner functions back to the caller


add, show = make_scoreboard("Lions")

print(show())
print(add(3))
print(add(7))
print(show())

# A second scoreboard gets its own private score.
other_add, other_show = make_scoreboard("Tigers")
other_add(1)
print(other_show())
print(show())                    # the Lions are unaffected

# ✅ Expected output:
# Lions: 0
# 3
# 10
# Lions: 10
# Tigers: 1
# Lions: 10

1) nonlocal — without it, score += points raises UnboundLocalError: cannot access local variable 'score' where it is not associated with a value, because assigning anywhere in a function makes that name local to it.

2) team — captured from the outer function's parameter.

3) show — you return both functions as a tuple, and the caller unpacks them.

If the Tigers line prints Tigers: 11, you are somehow sharing state between the two scoreboards — each call to make_scoreboard must create its own score.

🏆 Mini-Challenge: A Running Average

Outline only — you write the code. This one hides a subtlety worth meeting now rather than at 2am: when you change a captured object you need no keyword at all; nonlocal is only for rebinding a captured name to something new.

# 🎯 MINI-CHALLENGE: a running average with no class and no globals
#
# Write make_averager(). Calling it gives you back a function; every time you
# call THAT function with a number, it returns the mean of every number it has
# been given so far, rounded to 2 decimal places.
#
# 1. Inside make_averager, create an empty list called values
# 2. Define an inner function add(value) that:
#       - appends value to values
#       - returns round(sum(values) / len(values), 2)
# 3. Return add
# 4. avg = make_averager(), then print avg(10), avg(20), avg(30) and avg(0)
# 5. Finally print make_averager()(5) to show a brand-new averager starts fresh
#
# One thing to notice while you write it: you do NOT need `nonlocal` here.
# `values.append(...)` CHANGES the list you already have. `values = [...]`
# would REBIND the name, and that is the only case nonlocal exists for.
#
# ✅ Expected output:
# 10.0
# 15.0
# 20.0
# 15.0
# 5.0

# your code here
def make_averager():
    values = []

    def add(value):
        values.append(value)
        return round(sum(values) / len(values), 2)

    return add


avg = make_averager()
print(avg(10))
print(avg(20))
print(avg(30))
print(avg(0))

print(make_averager()(5))

Try the other version too — keep a total and a count as plain numbers instead of a list. That one does need nonlocal total and nonlocal count, because total += value rebinds the name. Same behaviour, and the difference between the two is the clearest explanation of nonlocal you will find.

⚙️ 5. Real Project Example #2 — A Configurable Logger

This is how real logging wrappers work:

def make_logger(level):
    def log(message):
        print(f"[{level}] {message}")
    return log

info = make_logger("INFO")
error = make_logger("ERROR")

info("Server started")
error("Connection failed")

# ✅ Expected output:
# [INFO] Server started
# [ERROR] Connection failed

This pattern is used in:

🔐 6. Real Project Example #3 — Authentication Middleware

def require_auth(role):
    def decorator(func):
        def wrapper(user, *args):
            if user.get("role") != role:
                raise PermissionError("Access denied")
            return func(user, *args)
        return wrapper
    return decorator

@require_auth("admin")
def delete_user(user, user_id):
    print(f"Deleting user {user_id}")

admin = {"role": "admin"}
delete_user(admin, 123)

# ✅ Expected output:
# Deleting user 123

This is exactly how Flask decorators, FastAPI dependencies, permission systems, and API gateways work behind the scenes.

🚀 7. Real Project Example #4 — Custom Data Validators

def range_validator(min_val, max_val):
    def validate(value):
        return min_val <= value <= max_val
    return validate

age_valid = range_validator(18, 100)
print(age_valid(25))  # True
print(age_valid(5))   # False

# ✅ Expected output:
# True
# False

⚡ 8. Real Project Example #5 — Caching with Closure State

def memoize(func):
    cache = {}
    def wrapper(x):
        if x not in cache:
            cache[x] = func(x)
        return cache[x]
    return wrapper

@memoize
def expensive_calc(n):
    print("Computing...")
    return n * n

print(expensive_calc(5))  # Computing... 25
print(expensive_calc(5))  # 25 (cached)

# ✅ Expected output:
# Computing...
# 25
# 25

⚡ 9. Real Project Example #6 — Rate Limiting API Calls

This is how APIs prevent spam:

import time

def rate_limiter(max_calls, period):
    calls = []
    def decorator(func):
        def wrapper(*args, **kwargs):
            nonlocal calls
            now = time.time()
            calls = [t for t in calls if now - t < period]
            if len(calls) >= max_calls:
                raise Exception("Rate limit exceeded")
            calls.append(now)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@rate_limiter(3, 5)
def api_call():
    print("API called")

api_call()
api_call()
api_call()

🧬 10. Real Project Example #7 — Dynamic Query Generators

def query_builder(table):
    def query(**filters):
        conditions = " AND ".join(f"{k}='{v}'" for k, v in filters.items())
        return f"SELECT * FROM {table} WHERE {conditions}"
    return query

users = query_builder("users")
print(users(age=25, active=True))

# ✅ Expected output:
# SELECT * FROM users WHERE age='25' AND active='True'

Closures here enable ORM-like systems, flexible APIs, and dashboard filtering.

🔄 11. Function Composition Using Closures

def compose(f, g):
    def composed(x):
        return f(g(x))
    return composed

def double(x): return x * 2
def add_5(x): return x + 5

pipeline = compose(double, add_5)
print(pipeline(10))  # (10+5)*2 = 30

# ✅ Expected output:
# 30

This powers data pipelines, ML preprocessing, and functional programming styles.

🧩 12. Closures vs Classes — When to Use Which?

🤔 The Decision:

Both closures and classes can store state. But closures are lightweight (just a function), while classes are feature-rich (methods, inheritance, etc.). Choose based on complexity!

Use Closures When...Use Classes When...
You need lightweight state (counter, cache)You have complex data with many attributes
The behavior is more important than the dataYou need inheritance or polymorphism
You want simple factoriesYou have many methods that interact
Performance matters (closures are faster)You need reusable objects with identity
# CLOSURE approach - simple, lightweight
def make_counter():
    count = 0
    def inc():
        nonlocal count
        count += 1
        return count
    return inc

# CLASS approach - more features
class Counter:
    def __init__(self):
        self.count = 0
    
    def inc(self):
        self.count += 1
        return self.count
    
    def reset(self):  # Easy to add methods!
        self.count = 0

# Both work, choose based on needs!
closure_counter = make_counter()
class_counter = Counter()

print(closure_counter())  # 1
print(class_counter.inc())  # 1

# ✅ Expected output:
# 1
# 1

💡 Modern codebases often mix both. Use closures for quick utilities, classes for complex domains.

🧠 13. How Python Stores Closure Data

def make_adder(x):
    def add(y):
        return x + y
    return add

add_5 = make_adder(5)
print(add_5.__closure__)
print(add_5.__closure__[0].cell_contents)

This is how Python tracks lexical scope.

🔥 14. Common Mistakes (and How to Avoid Them)

❌ MistakeWhat Happens✅ Fix
Missing nonlocalUnboundLocalErrorAdd nonlocal variable_name
Capturing loop variableAll functions share last valueUse default argument: def f(i=i)
Using globals insteadHard to test, not isolatedUse closure state instead

❌ Mistake #1: Modifying without nonlocal

def counter_broken():
    count = 0
    def inc():
        # count += 1  # ❌ UnboundLocalError!
        # Python thinks 'count' is a NEW local variable
        pass
    return inc

# The fix:
def counter_fixed():
    count = 0
    def inc():
        nonlocal count  # ✅ "Use the outer count!"
        count += 1
        return count
    return inc

c = counter_fixed()
print(c())  # 1
print(c())  # 2

# ✅ Expected output:
# 1
# 2

❌ Mistake #2: Loop variable capture (Tricky!)

# ❌ WRONG - all functions capture the SAME 'i'
funcs_bad = []
for i in range(3):
    funcs_bad.append(lambda: i)  # All will print 2!

print([f() for f in funcs_bad])  # [2, 2, 2] - oops!

# ✅ FIX - capture 'i' as a default argument
funcs_good = []
for i in range(3):
    funcs_good.append(lambda i=i: i)  # Each captures its own 'i'

print([f() for f in funcs_good])  # [0, 1, 2] - correct!

# ✅ Expected output:
# [2, 2, 2]
# [0, 1, 2]

🎯 15. Real-World Mini Project — Event Handler System

def event_system():
    handlers = {}
    
    def on(event, callback):
        if event not in handlers:
            handlers[event] = []
        handlers[event].append(callback)
    
    def emit(event, data):
        if event in handlers:
            for callback in handlers[event]:
                callback(data)
    
    return on, emit

subscribe, publish = event_system()

subscribe("login", lambda u: print(f"Welcome {u}"))
subscribe("login", lambda u: print(f"Logging {u}"))

publish("login", "Alice")

# ✅ Expected output:
# Welcome Alice
# Logging Alice

🔮 Part 2 — Advanced Production Patterns

You've mastered the basics. Now let's explore how senior engineers use closures in large-scale systems.

🔮 16. Using Closures for Dependency Injection

Most dependency injection systems in other languages require containers, service providers, and registries. Python can do it with one function.

def provide_db(connector):
    def run_query(q):
        db = connector()
        return db.execute(q) if hasattr(db, 'execute') else f"Query: {q} on {db}"
    return run_query

def sqlite_connector():
    return "SQLite connection"

def mysql_connector():
    return "MySQL connection"

query_local = provide_db(sqlite_connector)
query_prod = provide_db(mysql_connector)

print(query_local("SELECT * FROM users"))
print(query_prod("SELECT * FROM orders"))

# ✅ Expected output:
# Query: SELECT * FROM users on SQLite connection
# Query: SELECT * FROM orders on MySQL connection

Used in microservices, test environments, feature-flagged deployments, and plugin systems.

⚡ 17. Closures for Middleware (Flask, FastAPI, Starlette)

Every middleware stack follows one pattern:

def middleware(next_handler):
    def wrapper(request):
        print("Before")
        response = next_handler(request)
        print("After")
        return response
    return wrapper

def authenticate(next_handler):
    def wrapper(request):
        if not request.get("auth"):
            return "Unauthorized"
        return next_handler(request)
    return wrapper

def endpoint(request):
    return f"Hello {request.get('user', 'Guest')}"

# Chain multiple
handler = middleware(authenticate(endpoint))
print(handler({"auth": True, "user": "Alice"}))

# ✅ Expected output:
# Before
# After
# Hello Alice

This is how FastAPI Dependency Injection, Flask Decorators, Django Middleware, and Starlette Routing all work internally.

🎛 18. Closures to Build Retry, Timeout, Backoff Systems

import time
import random

def retry(times, delay=1):
    def decorator(func):
        def wrapper(*a, **kw):
            for i in range(times):
                try:
                    return func(*a, **kw)
                except Exception as e:
                    print(f"Attempt {i+1} failed: {e}")
                    time.sleep(delay)
            raise Exception("Failed after retries")
        return wrapper
    return decorator

@retry(3, delay=0.1)
def unstable_api():
    if random.random() < 0.7:
        raise Exception("API failed")
    return "Success"

try:
    print(unstable_api())
except Exception as e:
    print(e)

Cloud-based systems (AWS, GCP, Stripe, PayPal, Twilio) ALL use retry + exponential backoff to prevent failures.

📦 19. Closures for Local Caching With Expiration

import time

def timed_cache(seconds):
    def decorator(func):
        cache = {}
        timestamps = {}

        def wrapper(*a):
            if a in cache and time.time() - timestamps[a] < seconds:
                print("Cache hit!")
                return cache[a]
            result = func(*a)
            cache[a] = result
            timestamps[a] = time.time()
            return result

        return wrapper
    return decorator

@timed_cache(5)
def expensive_calc(x):
    print("Computing...")
    return x * x

print(expensive_calc(5))  # Computing
print(expensive_calc(5))  # Cache hit

Used in ML inference servers, recommendation systems, data dashboards, and pricing engines.

🧠 20. Using Closures to Build Feature Flags (A/B Testing)

def feature_flag(enabled):
    def decorator(func):
        def wrapper(*a, **kw):
            if enabled:
                return func(*a, **kw)
            return "Feature disabled"
        return wrapper
    return decorator

@feature_flag(False)
def new_checkout():
    return "New checkout flow!"

@feature_flag(True)
def new_dashboard():
    return "New dashboard!"

print(new_checkout())  # Feature disabled
print(new_dashboard())  # New dashboard!

# ✅ Expected output:
# Feature disabled
# New dashboard!

This mirrors real A/B testing systems at Netflix, Facebook, and Shopify.

🧬 21. Closures for Analytics Tracking

def tracker(event_name):
    def decorator(func):
        def wrapper(*a, **kw):
            print(f"[TRACK] {event_name}")
            return func(*a, **kw)
        return wrapper
    return decorator

@tracker("user_signup")
def register_user(email):
    print(f"Registering {email}")

register_user("[email protected]")

# ✅ Expected output:
# [TRACK] user_signup
# Registering [email protected]

Used in Mixpanel, Firebase Analytics, and Amplitude.

🧩 22. Using Closures to Build Mini Frameworks

Frameworks like Flask, FastAPI, Click, and Typer are closure-heavy.

COMMANDS = {}

def command(name):
    def decorator(func):
        COMMANDS[name] = func
        return func
    return decorator

@command("hello")
def hello():
    print("Hello world!")

@command("add")
def add():
    print(1 + 1)

# Execute
COMMANDS["hello"]()
COMMANDS["add"]()
print("Available commands:", list(COMMANDS.keys()))

# ✅ Expected output:
# Hello world!
# 2
# Available commands: ['hello', 'add']

Closures → registry → framework. You just built something similar to CLI libraries, routing systems, and plugin engines.

🛠 23. Function Pipelines Using Closures

def pipeline(*steps):
    def run(value):
        for step in steps:
            value = step(value)
        return value
    return run

def trim(x): return x.strip()
def lower(x): return x.lower()
def reverse(x): return x[::-1]

clean = pipeline(trim, lower, reverse)
print(clean("  HELLO  "))  # olleh

# ✅ Expected output:
# olleh

Used by Pandas, Spark, ML preprocessing, and data validation systems.

⚙️ 24. Closures for Automatic Resource Cleanup

def managed_resource(resource):
    def wrapper(func):
        def run(*a, **kw):
            r = resource()
            try:
                return func(r, *a, **kw)
            finally:
                r.close()
        return run
    return wrapper

class MockResource:
    def close(self):
        print("Resource closed")

@managed_resource(MockResource)
def use_resource(r):
    print("Using resource")

use_resource()

# ✅ Expected output:
# Using resource
# Resource closed

Used with database connections, file streams, and cache layers.

🧪 25. Closures for Test Fixtures

def fixture(setup):
    def decorator(func):
        def wrapper():
            env = setup()
            return func(env)
        return wrapper
    return decorator

def create_env():
    return {"db": "mock_db"}

@fixture(create_env)
def test_user(env):
    assert env["db"] == "mock_db"
    print("Test passed!")

test_user()

# ✅ Expected output:
# Test passed!

Closures = injectable test environments.

🌀 26. Closures for GUI & Game Event Systems

def on_click(message):
    def handler():
        print(message)
    return handler

# Bind in games
button_handler = on_click("Start!")
button_handler()  # Start!

pause_handler = on_click("Paused")
pause_handler()

# ✅ Expected output:
# Start!
# Paused

Closures store level states, UI states, and game event metadata.

🔍 27. Debugging Closures in Large Systems

import inspect

def make_adder(x):
    def add(y):
        return x + y
    return add

adder = make_adder(10)
print(adder.__closure__)
print(inspect.getclosurevars(adder))

Useful for debugging decorators, factories, async pipelines, and cached layers.

⚠️ 28. The "Late Binding" Bug & How to Fix It

funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])  # [2, 2, 2] - not [0,1,2]

# ✅ Expected output:
# [2, 2, 2]

Fix with early binding:

funcs = [(lambda x: lambda: x)(i) for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]

# ✅ Expected output:
# [0, 1, 2]

This is one of the most common closure bugs in the world.

⚡ 29. When NOT to Use Closures

🔮 Part 3 — The Deepest Level

You've expanded your closure knowledge. Now let's explore expert-level patterns used in AI pipelines, backends, and production systems.

⚡ 30. Async Closures — Combining AsyncIO + Lexical Scope

import asyncio

def async_retry(times):
    def decorator(func):
        async def wrapper(*a, **kw):
            for _ in range(times):
                try:
                    return await func(*a, **kw)
                except Exception:
                    await asyncio.sleep(0.1)
            raise Exception("Failed after retries")
        return wrapper
    return decorator

@async_retry(3)
async def unstable_fetch():
    return "Data fetched"

# In async context:
# result = await unstable_fetch()
print("Async retry decorator defined")

# ✅ Expected output:
# Async retry decorator defined

Used in websocket reconnection, unstable network fetches, async microservice calls, and task orchestration tools.

🧠 31. Building a Closure-Based State Machine

def state_machine(initial):
    state = initial

    def transition(new_state):
        nonlocal state
        state = new_state
        return state
    
    def current():
        return state
    
    return current, transition

get_state, set_state = state_machine("IDLE")

set_state("RUNNING")
set_state("PAUSED")
print(get_state())  # PAUSED

# ✅ Expected output:
# PAUSED

This pattern runs boss AI in games, dialogue systems, backend workflow state, and user authentication flow.

🚀 32. Closure-Driven ML Pipelines

def scaler(mean, std):
    def transform(x):
        return (x - mean) / std
    return transform

def clip(min_val, max_val):
    def transform(x):
        return max(min_val, min(x, max_val))
    return transform

# Chain transformations
normalize = scaler(120, 30)
clamp = clip(0, 255)

data = 150
result = clamp(normalize(data))
print(result)

# ✅ Expected output:
# 1.0

This powers preprocessing, augmentation, feature engineering, and batch transforms.

🔍 33. Closures for Compiler-Style Token Processing

def token_rule(pattern, action):
    def processor(token):
        if pattern(token):
            return action(token)
        return token
    return processor

is_number = lambda t: t.isdigit()
to_int = lambda t: int(t)

process_token = token_rule(is_number, to_int)
print(process_token("123"))  # 123
print(process_token("abc"))  # abc

# ✅ Expected output:
# 123
# abc

This mimics syntax highlighters, linting engines, formatters, and interpreters.

🕸 34. Microservice Routing Using Closure Factories

ROUTES = {}

def route(path):
    def decorator(func):
        ROUTES[path] = func
        return func
    return decorator

@route("/hello")
def hello():
    return {"msg": "world"}

@route("/users")
def users():
    return {"users": []}

print(ROUTES["/hello"]())

# ✅ Expected output:
# {'msg': 'world'}

This pattern appears in Flask, FastAPI, Node.js Express equivalents, and API gateways.

⏳ 35. Task Scheduling System (Cron-like)

import time

def schedule(interval):
    def decorator(func):
        last_run = [0]  # Use list for mutable state
        def wrapper():
            if time.time() - last_run[0] >= interval:
                last_run[0] = time.time()
                return func()
            return "Too soon!"
        return wrapper
    return decorator

@schedule(1)
def update_prices():
    return "Updating prices..."

print(update_prices())
print(update_prices())  # Too soon!

Used for price updates, leaderboard refresh, background jobs, and monitoring tasks.

🧬 36. Building a Custom ORM Layer Using Closures

def field(name):
    def getter(obj):
        return obj[name]
    return getter

user_name = field("name")
user_age = field("age")

user = {"name": "Alice", "age": 25}
print(user_name(user))  # Alice
print(user_age(user))   # 25

# ✅ Expected output:
# Alice
# 25

This allows dynamic model creation, field injection, serialization/deserialization, and validation.

🔄 37. Declarative UI Logic (React-like) With Closures

def use_state(init):
    state = [init]  # Use list for mutable state
    def get(): return state[0]
    def set(v):
        state[0] = v
    return get, set

get_count, set_count = use_state(0)
set_count(5)
print(get_count())  # 5
set_count(10)
print(get_count())  # 10

# ✅ Expected output:
# 5
# 10

Used in game UIs, terminal apps, custom dashboards, and educational tools.

🔐 38. Building Permission Systems Using Closure Capture

def require_role(role):
    def decorator(func):
        def wrapper(user, *a):
            if user.get("role") != role:
                raise PermissionError("Forbidden")
            return func(user, *a)
        return wrapper
    return decorator

@require_role("admin")
def delete_user(user, target_id):
    print("Deleting", target_id)

admin = {"role": "admin"}
delete_user(admin, 123)

# ✅ Expected output:
# Deleting 123

This powers admin dashboards, e-commerce backends, and authentication gateways.

📦 39. Closure-Based Message Queues

def message_queue():
    queue = []
    
    def publish(msg):
        queue.append(msg)
    
    def consume():
        if queue:
            return queue.pop(0)
    
    return publish, consume

send, receive = message_queue()
send("Hello")
send("World")
print(receive())  # Hello
print(receive())  # World

# ✅ Expected output:
# Hello
# World

Used for simulation, job queues, event systems, and async workers.

🧮 40. Mathematical Function Generators

def polynomial(a, b, c):
    def f(x):
        return a*x*x + b*x + c
    return f

quadratic = polynomial(1, -3, 2)
print(quadratic(0))  # 2
print(quadratic(1))  # 0
print(quadratic(2))  # 0

# ✅ Expected output:
# 2
# 0
# 0

Used in physics simulation, rendering engines, machine learning, and game movement curves.

🧩 41. Partial Application (Custom Implementation)

def partial(func, *preset):
    def wrapper(*a):
        return func(*preset, *a)
    return wrapper

def add(a, b, c):
    return a + b + c

add_5 = partial(add, 5)
print(add_5(10, 2))  # 17

# ✅ Expected output:
# 17

Alternate to functools.partial, giving Python the power of functional programming and cleaner callbacks.

🎛 42. "Middleware Stack" Engine Using Closures

Used in web servers, request filtering, AI agent chains, and on-device pipelines.

👁 43. Closures for Observers / Watchers (Reactive Programming)

Used in UI systems, stock trackers, game events, and reactive dashboards.

🧬 44. Closure-Based Memoization With Custom Invalidation

Better than lru_cache when you need dynamic TTL, external invalidation, or distributed system caching.

🎉 Conclusion — Full Mastery Achieved

You now understand the deepest real-world closure techniques, used in:

You've reached expert-level closure mastery used by senior Python engineers in production systems.

📋 Quick Reference — Closures

PatternWhat it does
def outer():\n def inner():Define a closure (inner remembers outer's vars)
nonlocal xModify a variable from the outer scope
outer()()Call the returned inner function
functools.partial(fn, x)Partially apply arguments to a function
Closure factoryOuter function returns configured inner function

🏆 Lesson Complete!

You now understand how closures capture state and how Python resolves variable scope — a key skill behind decorators, factories, and callback systems.

Practice quiz

What is lexical scope?

  • Variable visibility decided by where code RUNS
  • A type of global variable
  • Variable visibility decided by where code is WRITTEN
  • A way to import modules

Answer: Variable visibility decided by where code is WRITTEN. Lexical scope means visibility is determined by where code is written, not where it runs.

What is a closure?

  • An inner function that remembers variables from its enclosing scope
  • A function that takes no arguments
  • A way to close a file
  • A built-in Python keyword

Answer: An inner function that remembers variables from its enclosing scope. A closure is an inner function that captures and remembers variables from its outer function.

Which keyword lets an inner function MODIFY a variable from the enclosing function?

  • global
  • static
  • extern
  • nonlocal

Answer: nonlocal. nonlocal tells Python to modify the outer (enclosing) variable instead of creating a new local one.

Given make_multiplier(factor) returning multiply(x)=x*factor, what does make_multiplier(10)(3) return?

  • 13
  • 30
  • 10
  • 3

Answer: 30. factor=10 is captured, so multiply(3) returns 3 * 10 = 30.

What happens if you do count += 1 inside an inner function WITHOUT declaring nonlocal count?

  • It raises an UnboundLocalError
  • It works fine
  • It modifies a global
  • It returns None

Answer: It raises an UnboundLocalError. Without nonlocal, Python treats count as a new local, so reading it before assignment raises UnboundLocalError.

Given compose(f, g) returning f(g(x)), with double(x)=x*2 and add_5(x)=x+5, what does compose(double, add_5)(10) print?

  • 25
  • 20
  • 30
  • 15

Answer: 30. g runs first: add_5(10)=15, then double(15)=30.

What does [lambda: i for i in range(3)] then [f() for f in funcs] print (the late-binding bug)?

  • [0, 1, 2]
  • [2, 2, 2]
  • [1, 2, 3]
  • [0, 0, 0]

Answer: [2, 2, 2]. All lambdas share the same i, which ends at 2 after the loop, so every call returns 2.

How do you fix the loop-variable capture bug?

  • Use nonlocal
  • Use global
  • Use a tuple
  • Use a default argument like lambda i=i: i

Answer: Use a default argument like lambda i=i: i. lambda i=i: i captures the current value of i as a default, giving each lambda its own copy.

Where does Python store a closure's captured variables?

  • In __dict__
  • In the function's __closure__ cell objects
  • In a global registry
  • In the stack

Answer: In the function's __closure__ cell objects. Captured variables live in cell objects accessible via the function's __closure__ attribute.

Given a polynomial factory f(x)=a*x*x+b*x+c made with polynomial(1, -3, 2), what does the function return for x=2?

  • 2
  • 4
  • 0
  • -3

Answer: 0. 1*4 + (-3)*2 + 2 = 4 - 6 + 2 = 0.

Continue this course