Skip to main content

Scope, closures & decorators

Level 2: Level 2: Idiomsmedium12 minclosuresscopedecoratorsfunctions

Capture state in a closure and wrap behaviour with a decorator.

Why closures and decorators are everywhere

Every time you write @app.route, @pytest.fixture, or @functools.lru_cache, you are using both ideas at once. A closure lets a function carry state without a class or a global variable. A decorator lets you add behaviour (timing, retries, auth checks, caching) around a function without touching its body. In real codebases these keep cross-cutting logic in one place instead of copy-pasted into every function.

Scope: how Python resolves a name

Check yourself
count = 0 sits at module level. A function is defined as def bump(): print(count); count += 1. What happens when you call bump()?

When you use a name, Python searches four scopes in order: Local, Enclosing, Global, Built-in (LEGB). An inner function can read names from the enclosing function for free. To rebind one, you must declare it nonlocal; otherwise any assignment inside the function marks that name as local for the whole function, so reading it before the assignment runs raises UnboundLocalError.

A closure captures its enclosing scope

A closure is an inner function plus a live link to the variables it read from the function that created it. Those variables stay alive after the outer function returns.

def make_multiplier(factor):
    def multiply(x):
        return x * factor      # 'factor' comes from the enclosing scope
    return multiply

triple = make_multiplier(3)
print(triple(5))               # 15

multiply closes over factor. Because it keeps a reference to that variable (not a snapshot copy), you can build state that survives between calls:

def make_counter():
    count = 0
    def step():
        nonlocal count         # rebind the enclosing variable, not just read it
        count += 1
        return count
    return step

c = make_counter()
print(c(), c(), c())           # 1 2 3

Decorators wrap a function

Check yourself
double is a decorator that defines wrapper(x) returning fn(x) * 2 and then returns wrapper. You write @double on the line above def identity(x): return x. Immediately after that def, what is the name identity bound to?
Call stack — identity(5) with @double
Step 1 / 5
wrapper(5)

@double replaced identity with wrapper; the call lands here

Writing @double makes the name identity point at wrapper. Calling identity(5) runs wrapper, which calls the original fn (returns 5), then doubles it to return 10.

A decorator is a higher-order function: it takes a function and returns a replacement. The demo below defines double, whose inner wrapper calls the original fn and doubles the result. Writing @double above identity is exactly identity = double(identity), so the name identity now points at wrapper, and identity(5) returns 10.

Check yourself
funcs = [lambda: i for i in range(3)]. What does [f() for f in funcs] give you?

Pitfall: late binding in loops

Closures capture the variable, not its value at definition time:

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

Every lambda shares the same i, which has reached 2 by the time you call them. Bind the value eagerly with a default argument: lambda i=i: i gives [0, 1, 2].

Check yourself
After the plain @double decorator is applied, what does identity.__name__ return?

Interview nuance: a naive decorator hides the function it wraps. After @double, identity.__name__ is "wrapper" and its docstring is None, which breaks debuggers, introspection, and some frameworks. The fix is to decorate wrapper with functools.wraps(fn), which copies __name__ and __doc__ from the original and sets __wrapped__ to point back at it, so the wrapped function still looks like itself.

Worked example (Python)
def double(fn):
    def wrapper(x):
        return fn(x) * 2
    return wrapper

@double
def identity(x):
    return x

print(identity(5))   # 10

Apply

Your turn

The task this lesson builds to.

Implement scaled(factor, value) using a closure: define an inner function that captures factor and multiplies its argument by it, then call that inner function on value and return the result.

scaled(3, 5) is 15.

2 hints and 4 automated checks are waiting in the workspace.

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Implement double_result(n): write a decorator double that doubles whatever its wrapped function returns, apply it to a function that returns its argument, and return the result for n.

double_result(5) is 10.

2 hints and 4 automated checks are waiting in the workspace.