Skip to content

Decorators: Meta-Programming for Functions

Decorators are one of Python’s most powerful and elegant features. At their core, they allow you to “wrap” a function with another piece of code, extending its behavior without modifying the original function’s source.

Think of a decorator like a gift wrap: it doesn’t change the gift inside, but it adds a new layer (the paper and ribbon) that changes how the gift is presented or handled.


1. Prerequisites: Functions as First-Class Citizens

Section titled “1. Prerequisites: Functions as First-Class Citizens”

To understand decorators, you must accept that in Python, functions are objects. This means:

  • You can assign a function to a variable.
  • You can pass a function as an argument to another function.
  • You can define a function inside another function (Nested Functions).
  • A function can return another function.
higher_order.py
def get_multiplier(n):
def multiplier(x):
return x * n
return multiplier # Returning a function!
double = get_multiplier(2)
print(double(5)) # Output: 10

A decorator is a Higher-Order Function that takes a function and returns a new, “wrapped” version of that function.

The @decorator syntax is actually just “syntactic sugar” for func = decorator(func).

manual_decorator.py
def logger(func):
def wrapper():
print(f"Running {func.__name__}...")
func()
print("Done.")
return wrapper
@logger
def say_hi():
print("Hi!")
# Calling say_hi() now calls the 'wrapper' inside logger
say_hi()

When you wrap a function, the original function’s metadata (its name, docstring, etc.) is hidden by the wrapper. To fix this, Python provides a decorator called wraps in the functools module.

proper_decorator.py
from functools import wraps
def debug(func):
@wraps(func) # This preserves the metadata of the original 'func'
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@debug
def add(a, b):
"""Adds two numbers."""
return a + b
print(add.__name__) # 'add' (Without @wraps, this would be 'wrapper')

4. Decorators with Arguments (Decorator Factories)

Section titled “4. Decorators with Arguments (Decorator Factories)”

What if you want to pass a configuration to your decorator? (e.g., @repeat(3)). This requires a third level of nesting. The outer function takes the arguments and returns the decorator itself.

decorator_factory.py
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(name):
print(f"Hello, {name}")
greet("Alice") # Prints 'Hello, Alice' three times

5. Real-World Use Case: Authentication & Timing

Section titled “5. Real-World Use Case: Authentication & Timing”

In web development, decorators are often used to ensure a user is logged in before allowing access to a function.

auth_example.py
def require_admin(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_admin"):
raise PermissionError("Access Denied!")
return func(user, *args, **kwargs)
return wrapper
@require_admin
def delete_database(user):
print("Database deleted.")
# delete_database({"is_admin": False}) # Raises PermissionError

How does the wrapper function remember the original func even after the decorator function has finished running?

The answer is a Closure. When the wrapper is defined inside the decorator, it “captures” the local variables (including func) from its surrounding scope. Even when the decorator finishes, the wrapper carries that scope around with it like a backpack.

Decorators add a small amount of overhead because every call now involves an extra function call (the wrapper). In 99.9% of cases, this is negligible. However, in extremely tight, performance-critical loops, you might choose to avoid them.


  • Always use @wraps: It’s a small step that prevents debugging nightmares later.
  • Keep it Focused: A decorator should do one thing (e.g., log, time, authenticate).
  • Don’t Overuse: Too many decorators stacked on one function can make the code’s flow difficult to trace.
  • Consider Class-Based Decorators: If your decorator needs to maintain complex state, it might be easier to write it as a class with a __call__ method.