Module 12 β Closures & Decorators
Functions that remember, and functions that wrap other functions.
Prerequisite: Module 11 β Functional Programming
After this lesson, you will be able to
- Explain what a closure is and why an inner function can 'remember' outer variables
- Write a function that returns another function
- Write and apply a decorator using the @ syntax
Concept & Syntax
These two ideas trip up more beginners than almost anything else in this course, so take it slowly.
A closure happens when an inner function uses a variable from its enclosing (outer) function, and keeps access to it even after the outer function has finished running. It's how a function can carry its own private memory around with it.
A decorator is a function that takes another function as input, and returns a new function
that usually calls the original β plus does something extra before or after. The @decorator syntax
above a function definition is just a shortcut for my_function = decorator(my_function).
def decorator(func):
def wrapper(*args, **kwargs):
# do something before
result = func(*args, **kwargs)
# do something after
return result
return wrapper
@decorator
def my_function():
...Examples
A function returning a function Very Easy
def make_multiplier(factor):
def multiply(number):
return number * factor
return multiply
times3 = make_multiplier(3)
print(times3(10))make_multiplier(3) returns a brand-new function, multiply, that has 'remembered' factor = 3 β that's the closure.
A closure keeping private state Intermediate
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
count_up = make_counter()
print(count_up())
print(count_up())
print(count_up())count lives inside make_counter's scope, but counter() keeps a private, persistent reference to it via nonlocal β each call remembers the last.
Your first decorator Real World
def announce(func):
def wrapper():
print("Starting...")
func()
print("Done!")
return wrapper
@announce
def say_hi():
print("Hi!")
say_hi()@announce is shorthand for say_hi = announce(say_hi). Calling say_hi() now actually runs wrapper(), which adds behavior before and after the original function.
A decorator that accepts arguments Challenge
def announce(func):
def wrapper(*args, **kwargs):
print("Starting...")
result = func(*args, **kwargs)
print("Done!")
return result
return wrapper
@announce
def add(a, b):
return a + b
print(add(2, 3))Using *args and **kwargs in wrapper lets the decorator work on any function, regardless of what arguments it takes.
Visual Explanation
π‘ Why does this work?
Read the concept explanation above again slowly, line by line β every keyword there maps directly onto a line of code in the examples. If it still doesn't click, re-run the traced example (if this lesson has one) one step at a time.
π What happens internally? (advanced)
Under the hood, Python compiles your source into bytecode and runs it on a virtual machine (the CPython interpreter). Variables are names in a namespace dictionary pointing at objects in memory β which is exactly what the memory panel in the tracer above is showing you.
Common Mistakes
Forgetting to return the result of func() inside a wrapper silently discards the original return value.
def announce(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs) # result is thrown away!
return wrapper
@announce
def add(a, b):
return a + b
print(add(2, 3))def announce(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper- Always accept *args and **kwargs in a wrapper so the decorator works on any function signature.
- Always return the wrapped function's result from the wrapper.
- Use functools.wraps(func) on your wrapper to preserve the original function's name and docstring (a small but professional touch).
Exercise & Challenge
Exercise 1 β Predict the output
def outer():
message = "hi"
def inner():
return message.upper()
return inner
fn = outer()
print(fn())What does this print?
π Challenge
Write a decorator called timer_label that prints '[LOG] Running <function name>' before calling the wrapped function, then apply it to a simple function.
- Use *args/**kwargs so it works on any function
- Print the function's name using func.__name__
- Return the original function's result
π‘ Need a hint?
Every function object has a __name__ attribute.
π Show a sample solution
def timer_label(func):
def wrapper(*args, **kwargs):
print(f"[LOG] Running {func.__name__}")
return func(*args, **kwargs)
return wrapper
@timer_label
def add(a, b):
return a + b
print(add(2, 3))Quiz
Answer every question, then submit to see your score and explanations.
1. What is a closure?
2. What does @my_decorator above a function definition actually do?
3. Why should a decorator's wrapper accept *args and **kwargs?
Summary
- A closure is an inner function that keeps access to variables from its enclosing function.
- A decorator takes a function and returns a new function that wraps it with extra behavior.
- @decorator above a def is shorthand for func = decorator(func).
- Always forward *args/**kwargs and return the inner result in a decorator's wrapper.