Think of a decorator like gift wrap.
- The Decorator is the shiny paper and the card on top.
- The Function is the gift inside.
You can look at the card, check the weight, or add a “Happy Birthday” sticker before the gift is even opened. In code, decorators allow you to execute logic before and after a function runs, without touching the function’s internal code.
Basic Template
We use *args and **kwargs to make sure the decorator works with any function, regardless of how many inputs it has.
def my_decorator(func):
def wrapper(*args, **kwargs):
print("1. Logic before the function")
result = func(*args, **kwargs) # The gift is opened!
print("2. Logic after the function")
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
What is the @ symbol actually doing?
The @decorator syntax is what developers call “Syntactic Sugar.” This is just a fancy way of saying it’s a “sweeter,” shorter way to write something that would otherwise be a bit ugly.
Without the @ symbol, you would have to manually reassign your function.
def say_hello():
print("Hello!")
# This is what the @ symbol does behind the scenes:
say_hello = my_decorator(say_hello)
Using @wraps
When you wrap a function with a decorator, you are essentially replacing your function with the wrapper. By default, Python “forgets” the original function’s name, docstrings, etc. and starts using the wrapper’s info instead.
Without @wraps:
def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def say_hello():
"""This function says hello."""
print("Hello!")
print(say_hello.__name__) # Output: wrapper
print(say_hello.__doc__) # Output: None
@wraps ensures the function keeps its original identity (name, docstrings, etc.). Always use it.
from functools import wraps
def my_decorator(func):
# copies the metadata (the name, the docstrings, and the arguments)
# from the original function onto the wrapper.
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def say_hello():
"""This function says hello."""
print("Hello!")
print(say_hello.__name__) # Output: say_hello
print(say_hello.__doc__) # Output: This function says hello.
Practical Example
Decorators are perfect for security. Instead of checking if a user is logged in inside 50 different functions, you write one “Bouncer” decorator and reuse it.
user_is_logged_in = False
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not user_is_logged_in:
print("Access Denied: Please log in.")
return None
return func(*args, **kwargs)
return wrapper
@login_required
def view_account_balance():
print("Your balance is $5,000.")
view_account_balance() # Output: Access Denied
Decorators with Arguments
Sometimes your decorator needs its own settings (e.g., a specific multiplier or a custom log prefix). This requires a 3-Layer structure:
- Layer 1: The configuration layer (takes your settings).
- Layer 2: The actual decorator (takes the function).
- Layer 3: The wrapper (takes the arguments).
def multiplier(factor):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs) * factor
return wrapper
return decorator
@multiplier(3) # Triple the points!
def get_score():
return 10
print(get_score()) # Output: 30
Stacking
You can stack decorators! They apply from the bottom up (the one closest to the function runs first).
@logging_decorator
@timer_decorator # This one runs first
def my_function():
pass