Understand Python decorators from scratch. Learn to write your own decorators for logging, authentication, caching, and more.
Decorators are one of Python's most elegant features. They let you modify or extend functions and classes without changing their source code. If you've ever used @login_required in Django or @property in Python, you've already used decorators.
In Python, functions are first-class objects. You can assign them to variables, pass them as arguments, and return them from other functions:
def greet(name):
return f"Hello, {name}!"
# Assign function to a variable
say_hello = greet
print(say_hello("Django")) # "Hello, Django!"
# Pass function as argument
def call_twice(func, arg):
func(arg)
func(arg)
call_twice(greet, "World")
A decorator is a function that takes a function as input and returns a modified version of it:
def uppercase_decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@uppercase_decorator
def greet(name):
return f"Hello, {name}"
print(greet("Django")) # "HELLO, DJANGO"
@uppercase_decorator syntax is equivalent to writing greet = uppercase_decorator(greet). It replaces the original function with the wrapper.
Measure how long a function takes to execute:
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def process_data(n):
return sum(range(n))
process_data(1_000_000) # "process_data took 0.0312s"
@functools.wraps in your decorators! Without it, the decorated function loses its original name, docstring, and other metadata. This is especially important in Django, where debugging tools rely on function names.
To create a decorator that accepts arguments, you need an extra layer of nesting:
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hello():
print("Hello!")
say_hello()
# Hello!
# Hello!
# Hello!
import functools
import logging
logger = logging.getLogger(__name__)
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
logger.info(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
logger.info(f"{func.__name__} returned {result}")
return result
return wrapper
def cache(func):
"Simple memoization decorator"
_cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in _cache:
_cache[args] = func(*args)
return _cache[args]
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # Instant! Without cache this would take forever
@functools.cache and @functools.lru_cache built in. Use those instead of writing your own for production code.
Django uses decorators extensively. Here are the most common ones:
from django.contrib.auth.decorators import login_required, permission_required
from django.views.decorators.http import require_http_methods
from django.views.decorators.cache import cache_page
@login_required
def dashboard(request):
# Only authenticated users can access
return render(request, 'dashboard.html')
@permission_required('app.can_edit')
def edit_item(request, pk):
# Only users with 'can_edit' permission
...
@require_http_methods(["GET", "POST"])
def contact(request):
# Only GET and POST allowed
...
@cache_page(60 * 15) # Cache for 15 minutes
def product_list(request):
# Response cached, no DB hit for 15 minutes
...
You can apply multiple decorators to a single function. They execute from bottom to top:
@login_required # 2. Then check authentication
@require_http_methods(["POST"]) # 1. First check HTTP method
def delete_item(request, pk):
...
Decorators can also modify classes:
import functools
def singleton(cls):
"Ensure only one instance of a class exists"
instances = {}
@functools.wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class DatabaseConnection:
def __init__(self):
print("Connecting to database...")
db1 = DatabaseConnection() # "Connecting to database..."
db2 = DatabaseConnection() # No output - returns same instance
print(db1 is db2) # True
Decorators are a powerful pattern for adding reusable behavior to functions and classes:
@functools.wraps to preserve function metadata*args, **kwargs to make decorators work with any function signature