Decorators provide a way to modify the behavior of functions or methods without permanently altering them. A decorator is a function that takes another function as input, adds some functionality, and returns a new function with the enhanced behavior.
A decorator is essentially a wrapper around a function. It allows you to execute code before and after the wrapped function runs, without modifying the original function’s code.
def my_decorator(func):
"""A simple decorator that adds behavior."""
def wrapper():
print("Before the function runs")
func()
print("After the function runs")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
# When you call say_hello(), you're actually calling wrapper()
say_hello()
# Before the function runs
# Hello!
# After the function runs
@ SyntaxThe @ symbol is syntactic sugar—it’s a shorthand for applying a decorator to a function.
# These are equivalent:
# Using @ syntax
@my_decorator
def my_function():
pass
# Without @ syntax (manual decoration)
def my_function():
pass
my_function = my_decorator(my_function)
Both approaches produce the same result, but @ syntax is clearer and more commonly used.
Decorators rely on the fact that functions are first-class objects:
def makeuppercase(func):
"""Decorator that converts function result to uppercase."""
def wrapper():
result = func()
return result.upper()
return wrapper
@makeuppercase
def greet():
return "hello, world"
print(greet()) # HELLO, WORLD
If the original function takes arguments, the wrapper needs to accept and pass them along:
def log_call(func):
"""Decorator that logs function calls."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
@log_call
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(add(3, 5))
# Calling add with args=(3, 5), kwargs={}
# add returned 8
# 8
print(greet("Alice"))
# Calling greet with args=('Alice',), kwargs={}
# greet returned Hello, Alice!
# Hello, Alice!
print(greet("Bob", greeting="Hi"))
# Calling greet with args=('Bob',), kwargs={'greeting': 'Hi'}
# greet returned Hi, Bob!
# Hi, Bob!
*args and **kwargsThese special parameters allow the wrapper to accept any arguments:
*args collects all positional arguments into a tuple**kwargs collects all keyword arguments into a dictionary*args and **kwargs to func() forwards them to the original functionMeasure how long a function takes to execute:
import time
def time_it(func):
"""Decorator that times function execution."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@time_it
def slow_function():
time.sleep(1)
return "Done"
print(slow_function())
# slow_function took 1.0012 seconds
# Done
Add logging to function calls:
def debug(func):
"""Decorator that prints debug information."""
def wrapper(*args, **kwargs):
print(f"DEBUG: Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"DEBUG: {func.__name__} returned {result}")
return result
return wrapper
@debug
def calculate(x, y):
return x * y + 10
print(calculate(5, 3))
# DEBUG: Calling calculate
# DEBUG: calculate returned 25
# 25
Cache expensive function results to avoid recomputation:
def memoize(func):
"""Decorator that caches function results."""
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
"""Calculate the nth Fibonacci number (slow without memoization)."""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(100)) # Fast! Results are cached
Without memoization, fibonacci(100) would take exponentially longer. With memoization, each value is calculated only once.
Validate function arguments:
def validate_positive(func):
"""Decorator that ensures arguments are positive."""
def wrapper(x):
if x <= 0:
raise ValueError(f"Argument must be positive, got {x}")
return func(x)
return wrapper
@validate_positive
def calculate_square_root(x):
return x ** 0.5
print(calculate_square_root(9)) # 3.0
print(calculate_square_root(-1)) # ValueError: Argument must be positive, got -1
Register functions in a list or dictionary:
# Registry to store all command functions
commands = {}
def register_command(name):
"""Decorator that registers a function as a command."""
def decorator(func):
commands[name] = func
return func
return decorator
@register_command("help")
def show_help():
print("Available commands: help, status, quit")
@register_command("status")
def show_status():
print("System is running")
@register_command("quit")
def quit_app():
print("Goodbye!")
# Commands are automatically registered
print(commands.keys()) # dict_keys(['help', 'status', 'quit'])
# Execute a command by name
user_input = "help"
if user_input in commands:
commands[user_input]() # Available commands: help, status, quit
You can apply multiple decorators to a single function. Decorators are applied from bottom to top:
@decorator_one
@decorator_two
def my_function():
pass
# Is equivalent to:
my_function = decorator_one(decorator_two(my_function))
Example:
def make_bold(func):
def wrapper():
return f"<b>{func()}</b>"
return wrapper
def make_italic(func):
def wrapper():
return f"<i>{func()}</i>"
return wrapper
@make_bold
@make_italic
def greet():
return "Hello"
print(greet()) # <b><i>Hello</i></i></b>
The execution order is:
make_italic wraps greet → <i>Hello</i>make_bold wraps the result → <b><i>Hello</i></i></b>When you decorate a function, the wrapper replaces the original function. This can lose important metadata like the function name and docstring:
def my_decorator(func):
def wrapper():
return func()
return wrapper
@my_decorator
def important_function():
"""This is an important function."""
pass
print(important_function.__name__) # wrapper (not "important_function"!)
print(important_function.__doc__) # None (not the docstring!)
To preserve the original function’s metadata, use functools.wraps:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper():
return func()
return wrapper
@my_decorator
def important_function():
"""This is an important function."""
pass
print(important_function.__name__) # important_function
print(important_function.__doc__) # This is an important function.
Here’s a real-world example that retries failed API calls:
import time
from functools import wraps
def retry(max_attempts=3, delay=1):
"""Decorator that retries a function on failure."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise # Re-raise on final attempt
print(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay}s...")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def fetch_api_data(url):
"""Simulate an API call that might fail."""
import random
if random.random() < 0.7: # 70% chance of failure
raise ConnectionError("API unavailable")
return {"data": "success"}
print(fetch_api_data("https://api.example.com"))
# Might print: Attempt 1 failed: API unavailable. Retrying in 0.5s...
# Then either succeed or fail after max attempts
Forgetting to Return the Wrapper
# Wrong - decorator doesn't return anything
def bad_decorator(func):
def wrapper():
print("Running...")
func()
# Missing return wrapper!
# Correct
def good_decorator(func):
def wrapper():
print("Running...")
func()
return wrapper
Forgetting to Call the Original Function
# Wrong - wrapper doesn't call func()
def bad_decorator(func):
def wrapper():
print("Running...")
# Missing func() call!
return wrapper
# Correct
def good_decorator(func):
def wrapper():
print("Running...")
return func()
return wrapper
Forgetting to Return the Result
# Wrong - wrapper doesn't return func()'s result
def bad_decorator(func):
def wrapper(*args, **kwargs):
print("Running...")
func(*args, **kwargs) # Result is discarded!
return wrapper
# Correct
def good_decorator(func):
def wrapper(*args, **kwargs):
print("Running...")
return func(*args, **kwargs) # Return the result
return wrapper
Use decorators when you need to:
Don’t use decorators when: