Four concepts, one story: functions can be treated like values β that lets inner functions remember things β which lets us build decorators β which can be made configurable. Let's walk through it end to end.
First-Class Functions
β
Functions can be passed around & returned
β
Closures become possible
β
Closures help us build decorators
β
Decorators can accept parameters
β
functools.wraps preserves metadata
1οΈβ£ First-Class Functions
In Python, a function is just another value β you can store it, pass it, or return it, exactly like an int or a string.
def greet():
print("Hello!")
x = greet # NOT greet() β just the function itself
x() # Hello!
| Code | Meaning |
|---|---|
greet |
The function itself |
greet() |
Executes the function |
x = greet |
Stores the function in x
|
x = greet() |
Stores the return value of calling it |
Passing a function as an argument
def greet():
print("Hello!")
def execute(func):
func()
execute(greet) # Hello!
Real use: a generic perform_action() that works with any function you hand it:
def login(): print("Logging in")
def logout(): print("Logging out")
def perform_action(action):
action()
perform_action(login)
perform_action(logout)
Returning a function
def outer():
def inner():
print("Hello from inner")
return inner
x = outer()
x() # Hello from inner
This β a function that creates and returns another function β is the doorway into closures.
2οΈβ£ Closures π
A closure is a function that remembers variables from its surrounding scope, even after the outer function has already finished running.
Think of it like a parent passing a secret to their child before leaving β the child still remembers it long after the parent is gone.
def outer():
name = "Deepika"
def inner():
print(name)
return inner
func = outer()
func() # Deepika
outer() has already returned by the time we call func() β yet inner() still remembers name. That's the closure at work.
The 3 ingredients of a closure
| # | Requirement |
|---|---|
| 1 | A nested function |
| 2 | The inner function uses a variable from the outer scope |
| 3 | The outer function returns the inner function |
A more useful example
def multiplier(number):
def multiply(x):
return x * number
return multiply
double = multiplier(2)
triple = multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
double and triple are two separate closures β each remembers a different number.
3οΈβ£ Decorators π
A decorator is a function that modifies or extends another function's behavior without changing its original code.
def decorator(func):
def wrapper():
print("Before function")
func()
print("After function")
return wrapper
@decorator
def greet():
print("Hello!")
greet()
Before function
Hello!
After function
What @decorator actually does
@decorator
def greet():
print("Hello")
is literally shorthand for:
def greet():
print("Hello")
greet = decorator(greet)
decorator(greet) returns wrapper, and greet now points to wrapper instead of the original function.
4οΈβ£ Parameterized Decorators π―
What if the decorator itself needs an argument?
@repeat(3)
def greet():
print("Hello")
Here repeat(3) isn't the function being decorated β it's configuring the decorator. That requires one extra layer of nesting.
def repeat(times): # 1οΈβ£ receives the decorator's argument
def decorator(func): # 2οΈβ£ receives the actual function
def wrapper(): # 3οΈβ£ runs the function
for _ in range(times):
func()
return wrapper
return decorator
@repeat(3)
def greet():
print("Hello")
greet()
Hello
Hello
Hello
The three layers:
-
repeat(times)β capturestimes = 3 -
decorator(func)β capturesfunc = greet -
wrapper()β actually callsfunc(),timestimes
The key insight: it's a closure
wrapper() uses times, which belongs to repeat(times) β an outer function that has already returned. That's exactly the closure pattern from earlier.
repeat(3)
β
βββ times = 3
β
βββ decorator()
β
βββ wrapper()
β
βββ remembers times = 3
Parameterized decorators are built using closures. That's the connection worth remembering.
Another example β a message decorator:
def message(text):
def decorator(func):
def wrapper():
print(text)
func()
return wrapper
return decorator
@message("Welcome to Python!")
def greet():
print("Hello Deepika")
greet()
Welcome to Python!
Hello Deepika
5οΈβ£ The Metadata Problem π
def greet():
"""This function greets the user."""
print("Hello")
print(greet.__name__) # greet
print(greet.__doc__) # This function greets the user.
Now decorate it:
def decorator(func):
def wrapper():
func()
return wrapper
@decorator
def greet():
"""This function greets the user."""
print("Hello")
print(greet.__name__) # wrapper π³
Since greet = decorator(greet) makes greet point to wrapper, Python loses track of the original function's name and docstring.
The fix: functools.wraps
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper():
func()
return wrapper
@decorator
def greet():
"""This function greets the user."""
print("Hello")
print(greet.__name__) # greet
print(greet.__doc__) # This function greets the user.
@wraps(func) copies over metadata like __name__, __doc__, __module__, and __annotations__ from the original function onto the wrapper.
Why it matters: in a real app with 100 decorated functions, without @wraps every single one shows up as wrapper during debugging. With it, each keeps its real name β much easier to trace.
6οΈβ£ *args and **kwargs β Making Decorators Work with Any Function
A real decorator needs to handle functions with different signatures:
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Before function")
result = func(*args, **kwargs)
print("After function")
return result
return wrapper
Now it works whether the decorated function is add(10, 20), greet("Deepika"), or login(username="deepika", password="1234"). This is why almost every real-world decorator you'll see includes *args, **kwargs.
π§© Putting It All Together
from functools import wraps
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet():
"""Greets the user."""
print("Hello!")
greet()
print(greet.__name__) # greet
print(greet.__doc__) # Greets the user.
This one example uses all four concepts:
-
funcβ a function passed as a first-class value -
wrapper()rememberingtimesβ a closure -
@repeat(3)β a parameterized decorator -
@wraps(func)β preserving metadata
π Where This Shows Up in Real Code
| Application | What decorators do |
|---|---|
| π Authentication | Check whether a user is logged in |
| β±οΈ Performance | Measure execution time |
| π Logging | Record function calls |
| π Authorization | Check permissions |
| πΎ Caching | Store previous results (@lru_cache) |
| π Debugging | Track function behavior |
| π Web frameworks | Route functions to URLs β e.g. @app.route("/home")
|
| π Retry logic | Retry failed operations |
π― Interview-Ready Definitions
| Question | Answer |
|---|---|
| What is a first-class function? | A function that can be stored in a variable, passed as an argument, and returned from another function β treated like any other value |
| What is a closure? | An inner function that remembers and can access variables from its enclosing scope, even after the outer function has finished executing |
| What is a parameterized decorator? | A decorator that accepts its own arguments/configuration before receiving the function it decorates, e.g. @repeat(3)
|
Why use functools.wraps? |
It preserves the original function's metadata (name, docstring, etc.) when a decorator replaces it with a wrapper |
π§ One Mental Model to Hold On To
FUNCTION
β can be treated like a value
βΌ
FIRST-CLASS FUNCTION
β can be returned
βΌ
INNER FUNCTION
β remembers outer variables
βΌ
CLOSURE
β used to build
βΌ
DECORATOR
β accepts configuration
βΌ
PARAMETERIZED DECORATOR
β use @wraps
βΌ
CLEAN + PRESERVED METADATA
The go-to code skeleton for any configurable decorator:
from functools import wraps
def decorator(argument):
def actual_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# extra behavior before
result = func(*args, **kwargs)
# extra behavior after
return result
return wrapper
return actual_decorator
Once you can name what each of those three nested functions is doing, parameterized decorators stop looking scary. β€οΈ
Top comments (0)