- Published on
Python Decorators - The Magic Behind the @ Symbol
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Decorators are one of Python's most elegant features. They let you modify or enhance functions without changing their code. You've seen @app.route(), @staticmethod, @property — but do you know how they actually work?
In this guide, we'll go from basics to building real-world decorators.
- Functions are First-Class Objects
- Your First Decorator
- Preserving Function Metadata with functools.wraps
- Decorators with Arguments
- Real-World Example: Timing Decorator
- Real-World Example: Authentication Decorator
- Real-World Example: Caching Decorator
- Class-Based Decorators
- Stacking Multiple Decorators
- Conclusion
Functions are First-Class Objects
To understand decorators, you first need to know that in Python, functions are objects. They can be passed as arguments, returned from other functions, and assigned to variables.
def greet():
return "Hello!"
# Assigning a function to a variable
my_func = greet
print(my_func()) # "Hello!"
# Passing a function as an argument
def run(func):
return func()
print(run(greet)) # "Hello!"
Your First Decorator
A decorator is a function that takes another function as input and returns a modified version:
def my_decorator(func):
def wrapper():
print("Before the function runs")
func()
print("After the function runs")
return wrapper
def say_hello():
print("Hello!")
# Manually decorating
say_hello = my_decorator(say_hello)
say_hello()
# Output:
# Before the function runs
# Hello!
# After the function runs
The @ syntax is just shorthand for the above:
@my_decorator
def say_hello():
print("Hello!")
say_hello() # Same result as above
Preserving Function Metadata with functools.wraps
Without functools.wraps, your decorated function loses its name and docstring:
from functools import wraps
def my_decorator(func):
@wraps(func) # Always add this!
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
@my_decorator
def add(a, b):
"""Adds two numbers."""
return a + b
print(add.__name__) # "add" (not "wrapper")
print(add.__doc__) # "Adds two numbers."
Decorators with Arguments
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(times=3)
def say_hi():
print("Hi!")
say_hi()
# Hi!
# Hi!
# Hi!
Real-World Example: Timing Decorator
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} ran in {end - start:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "Done"
slow_function()
# slow_function ran in 1.0012s
Real-World Example: Authentication Decorator
from functools import wraps
def require_auth(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_authenticated"):
raise PermissionError("You must be logged in!")
return func(user, *args, **kwargs)
return wrapper
@require_auth
def get_dashboard(user):
return f"Welcome, {user['name']}!"
# Test
user = {"name": "Alice", "is_authenticated": True}
print(get_dashboard(user)) # "Welcome, Alice!"
guest = {"name": "Guest", "is_authenticated": False}
get_dashboard(guest) # Raises PermissionError
Real-World Example: Caching Decorator
from functools import wraps
def cache(func):
stored = {}
@wraps(func)
def wrapper(*args):
if args not in stored:
stored[args] = func(*args)
return stored[args]
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # Fast! Results are cached
Python's built-in
functools.lru_cachedoes the same thing and is even more powerful.
Class-Based Decorators
You can also create decorators using classes:
from functools import wraps
class retry:
def __init__(self, max_attempts=3):
self.max_attempts = max_attempts
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(self.max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
raise Exception("All attempts failed")
return wrapper
@retry(max_attempts=3)
def risky_call():
import random
if random.random() < 0.7:
raise ValueError("Random error")
return "Success!"
print(risky_call())
Stacking Multiple Decorators
from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def get_text():
return "Hello"
print(get_text()) # <b><i>Hello</i></b>
Decorators are applied bottom-up — @italic is applied first, then @bold.
Conclusion
Decorators are one of the most powerful patterns in Python. They let you add logging, caching, authentication, timing, and more to any function — without touching its code. Master decorators and you'll write cleaner, more reusable Python code every time.