- What are decorators — functions that add extra features to other functions
- Why use decorators — keep code clean, reusable, and organized
- How they work — the @ syntax and wrapper functions
- Create your own — build custom decorators
- Real-world examples — logging, timing, authentication
- Best practices — using
functools.wrapsand more
What are Decorators?
A decorator is a special function that adds extra features to another function. It's like a wrapper that goes around your function and adds something before or after it runs.
Think of a decorator like a gift wrapper. You have a gift (your function). You wrap it in nice paper (the decorator). The gift is still the same, but now it looks better and maybe has a card attached.
Decorators use the @ symbol in Python. You put @decorator_name right above your function definition.
Key concept: A decorator is a function that takes another function as an argument, adds some behavior, and returns the modified function.
Why Use Decorators?
The Benefits of Decorators
Decorators make your code cleaner and more organized. Here's why you should use them:
# Why Use Decorators?
print("=" * 50)
print("WHY USE DECORATORS?")
print("=" * 50)
# ============================================================
# WITHOUT DECORATORS — Repetitive Code
# ============================================================
print("\n WITHOUT DECORATORS:")
def greet(name):
print("=" * 20)
print(f"Hello {name}!")
print("=" * 20)
def farewell(name):
print("=" * 20)
print(f"Goodbye {name}!")
print("=" * 20)
greet("Alice")
farewell("Bob")
print("\n Problem: The border code is repeated in EVERY function")
print(" Problem: If we change the border, we change it everywhere")
# ============================================================
# WITH DECORATORS — Clean and Reusable
# ============================================================
print("\n WITH DECORATORS:")
def add_border(func):
"""Decorator that adds a border around the output"""
def wrapper(name):
print("=" * 20)
func(name)
print("=" * 20)
return wrapper
@add_border
def greet_with_border(name):
print(f"Hello {name}!")
@add_border
def farewell_with_border(name):
print(f"Goodbye {name}!")
greet_with_border("Alice")
farewell_with_border("Bob")
print("\n Benefits:")
print(" 1. No repetition — border code is in one place")
print(" 2. Easy to change — update the decorator once")
print(" 3. Clean code — functions focus on their main job")
print(" 4. Reusable — use the same decorator anywhere")
# ============================================================
# COMMON USES OF DECORATORS
# ============================================================
print("\n" + "-" * 30)
print("COMMON USES")
print("-" * 30)
print("""
┌─────────────────────┬────────────────────────────────────────────┐
│ USE CASE │ WHY IT HELPS │
├─────────────────────┼────────────────────────────────────────────┤
│ Logging │ Track when functions run │
│ Timing │ Measure how long functions take │
│ Authentication │ Check if user is logged in │
│ Caching │ Store results to avoid recalculating │
│ Rate limiting │ Limit how often a function can run │
│ Permission checks │ Check if user has permission │
└─────────────────────┴────────────────────────────────────────────┘
""")
Benefits of decorators:
- No repetition — write code once, use it many times
- Easy to maintain — change one place, not many
- Clean code — functions focus on their main job
- Reusable — use the same decorator on many functions
- Separates concerns — keeps different logic separate
Quick Check: What problem do decorators solve? (Answer: They help avoid repetitive code by adding common behavior to multiple functions)
How Decorators Work
The Magic Behind Decorators
To understand decorators, you need to know two things about Python:
- Functions are objects — you can pass them around like any other value
- Functions can be nested — you can define a function inside another function
A decorator is just a function that takes a function, wraps it, and returns the wrapped version.
# How Decorators Work
print("=" * 50)
print("HOW DECORATORS WORK")
print("=" * 50)
# ============================================================
# STEP 1: Functions are Objects
# ============================================================
print("\n1. FUNCTIONS ARE OBJECTS")
def say_hello(name):
return f"Hello {name}!"
# You can assign a function to a variable
greet = say_hello
print(f" say_hello('Alice'): {say_hello('Alice')}")
print(f" greet('Alice'): {greet('Alice')}")
# You can pass functions as arguments
def call_function(func, value):
return func(value)
print(f" call_function(say_hello, 'Bob'): {call_function(say_hello, 'Bob')}")
# ============================================================
# STEP 2: Nested Functions
# ============================================================
print("\n2. NESTED FUNCTIONS")
def outer_function(value):
print(f" Outer function called with: {value}")
def inner_function():
return f" Inner function using: {value}"
return inner_function
inner = outer_function("test")
print(inner())
# ============================================================
# STEP 3: Creating a Simple Decorator
# ============================================================
print("\n3. CREATING A SIMPLE DECORATOR")
def simple_decorator(func):
"""A simple decorator that adds a message"""
def wrapper():
print(" Before function")
func()
print(" After function")
return wrapper
# Manual way (without @)
def say_hi():
print(" Hi!")
decorated = simple_decorator(say_hi)
decorated()
# With @ (the Python way)
@simple_decorator
def say_hello_decorated():
print(" Hello!")
say_hello_decorated()
# ============================================================
# STEP 4: What the @ Does
# ============================================================
print("\n4. WHAT THE @ DOES")
# This code:
@simple_decorator
def my_function():
print(" Function body")
# Is exactly the same as:
def my_function():
print(" Function body")
my_function = simple_decorator(my_function)
print(" @ is just a shortcut!")
print(" The decorator replaces the original function with the wrapper")
# ============================================================
# THE COMPLETE PICTURE
# ============================================================
print("\n" + "-" * 30)
print("SUMMARY — HOW DECORATORS WORK")
print("-" * 30)
print("""
1. Python sees the @ symbol above your function
2. It takes your function and passes it to the decorator
3. The decorator returns a new function (the wrapper)
4. Your function name now points to the wrapper
5. When you call your function, the wrapper runs first
""")
How decorators work key points:
- Functions are objects — they can be passed as arguments
- Nested functions — functions can be defined inside other functions
- Wrapper pattern — the decorator creates a wrapper function
- @ syntax — shorthand for
func = decorator(func) - Replaces the original — the original function is replaced by the wrapper
Quick Check: What does the @ symbol do in Python? (Answer: It applies a decorator to a function, replacing the function with the decorated version)
Creating Your Own Decorators
Building Custom Decorators
Now let's create some useful decorators you can use in your projects.
# Creating Your Own Decorators
print("=" * 50)
print("CREATING YOUR OWN DECORATORS")
print("=" * 50)
# ============================================================
# DECORATOR 1: Timing Function Execution
# ============================================================
print("\n1. TIMING DECORATOR")
import time
def timer(func):
"""Decorator that times how long a function takes"""
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
@timer
def slow_function():
time.sleep(0.5)
return "Done"
print(f" Result: {slow_function()}")
# ============================================================
# DECORATOR 2: Logging
# ============================================================
print("\n2. LOGGING DECORATOR")
def logger(func):
"""Decorator that logs when a function is called"""
def wrapper(*args, **kwargs):
print(f" Calling {func.__name__}")
print(f" Arguments: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f" Result: {result}")
return result
return wrapper
@logger
def add_numbers(a, b):
return a + b
@logger
def greet_person(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(add_numbers(5, 3))
print(greet_person("Alice", greeting="Hi"))
# ============================================================
# DECORATOR 3: Retry on Error
# ============================================================
print("\n3. RETRY DECORATOR")
def retry(max_attempts=3, delay=1):
"""Decorator that retries a function if it fails"""
def decorator(func):
def wrapper(*args, **kwargs):
attempts = 0
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
print(f" Attempt {attempts} failed: {e}")
if attempts < max_attempts:
print(f" Retrying in {delay} seconds...")
time.sleep(delay)
raise Exception(f"Function failed after {max_attempts} attempts")
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def flaky_function():
import random
if random.random() < 0.6:
raise ValueError("Random failure!")
return "Success!"
print(" Trying flaky function:")
try:
result = flaky_function()
print(f" Result: {result}")
except Exception as e:
print(f" {e}")
# ============================================================
# DECORATOR 4: Authentication (Simple)
# ============================================================
print("\n4. AUTHENTICATION DECORATOR")
def require_login(func):
"""Decorator that checks if user is logged in"""
# In real code, this would check a session or token
def wrapper(*args, **kwargs):
is_logged_in = True # Simulate login status
if is_logged_in:
return func(*args, **kwargs)
else:
return " Authentication required!"
return wrapper
@require_login
def view_profile(user_id):
return f" Profile for user {user_id}"
@require_login
def edit_settings():
return " Settings updated"
print(f" {view_profile(123)}")
print(f" {edit_settings()}")
# ============================================================
# DECORATOR 5: Caching Results
# ============================================================
print("\n5. CACHING DECORATOR")
def cache(func):
"""Decorator that caches function results"""
cache_dict = {}
def wrapper(*args):
if args in cache_dict:
print(f" Using cached value for {args}")
return cache_dict[args]
result = func(*args)
cache_dict[args] = result
print(f" Computing new value for {args}")
return result
return wrapper
@cache
def expensive_function(x):
time.sleep(0.3) # Simulate expensive computation
return x * x
print(expensive_function(5)) # Computes
print(expensive_function(5)) # Uses cache
print(expensive_function(10)) # Computes
print(expensive_function(10)) # Uses cache
Custom decorators key points:
- timer — measures how long a function takes
- logger — logs function calls and arguments
- retry — retries a function if it fails
- require_login — checks authentication
- cache — stores results to avoid recomputation
Quick Check: What does the timer decorator do? (Answer: It measures and prints how long a function takes to run)
Decorators with Arguments
Passing Arguments to Decorators
Sometimes you want to customize your decorator. For example, you might want to specify how many times to retry, or what logging level to use.
To do this, you need a decorator factory — a function that returns a decorator.
# Decorators with Arguments
print("=" * 50)
print("DECORATORS WITH ARGUMENTS")
print("=" * 50)
# ============================================================
# DECORATOR WITH ARGUMENTS — Retry with custom attempts
# ============================================================
print("\n1. RETRY WITH CUSTOM ATTEMPTS")
def retry_with_attempts(attempts):
"""Decorator factory that retries a function"""
def decorator(func):
def wrapper(*args, **kwargs):
for attempt in range(attempts):
try:
return func(*args, **kwargs)
except Exception as e:
print(f" Attempt {attempt + 1} failed: {e}")
if attempt == attempts - 1:
raise
return None
return wrapper
return decorator
@retry_with_attempts(2)
def fragile_function():
raise ValueError("Something went wrong!")
try:
fragile_function()
except ValueError as e:
print(f" All attempts failed: {e}")
# ============================================================
# DECORATOR WITH ARGUMENTS — Log with custom message
# ============================================================
print("\n2. LOG WITH CUSTOM MESSAGE")
def log_with_message(message):
"""Decorator factory that logs with a custom message"""
def decorator(func):
def wrapper(*args, **kwargs):
print(f" {message}")
print(f" Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@log_with_message("Starting database operation...")
def save_to_db(data):
return f"Saved: {data}"
@log_with_message("Starting API call...")
def fetch_from_api():
return "API data"
print(save_to_db("user_data"))
print(fetch_from_api())
# ============================================================
# DECORATOR WITH ARGUMENTS — Rate Limiting
# ============================================================
print("\n3. RATE LIMITING")
def rate_limit(limit_per_second=1):
"""Decorator that limits how often a function can run"""
last_called = [0] # Use a list to store mutable state
def decorator(func):
def wrapper(*args, **kwargs):
current_time = time.time()
time_since_last = current_time - last_called[0]
if time_since_last < 1.0 / limit_per_second:
print(f" Rate limit hit. Wait {1.0/limit_per_second - time_since_last:.2f}s")
time.sleep(1.0/limit_per_second - time_since_last)
last_called[0] = time.time()
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(limit_per_second=2)
def send_message(msg):
return f" Sending: {msg}"
print(send_message("Hello"))
print(send_message("World"))
print(send_message("Test"))
# ============================================================
# UNDERSTANDING DECORATOR FACTORIES
# ============================================================
print("\n4. UNDERSTANDING DECORATOR FACTORIES")
print("""
┌─────────────────────────────────────────────────────────────────────┐
│ DECORATOR FACTORY — A Function That Returns a Decorator │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ def decorator_factory(arg1, arg2): │
│ def decorator(func): │
│ def wrapper(*args, **kwargs): │
│ # Do something with arg1, arg2 │
│ result = func(*args, **kwargs) │
│ return result │
│ return wrapper │
│ return decorator │
│ │
│ @decorator_factory("hello", 5) │
│ def my_function(): │
│ pass │
│ │
│ This allows you to customize decorators with arguments. │
└─────────────────────────────────────────────────────────────────────┘
""")
Decorators with arguments key points:
- Decorator factory — a function that returns a decorator
- Three levels — factory → decorator → wrapper
- Customization — pass arguments to control the decorator's behavior
- Common uses — retry attempts, log messages, rate limits
Quick Check: What is a decorator factory? (Answer: A function that takes arguments and returns a decorator)
Chaining Decorators
Using Multiple Decorators
You can apply multiple decorators to one function. They stack from bottom to top (closest to the function runs first).
# Chaining Decorators
print("=" * 50)
print("CHAINING DECORATORS")
print("=" * 50)
# ============================================================
# DEFINE SOME DECORATORS
# ============================================================
def bold(func):
"""Add bold formatting"""
def wrapper(*args, **kwargs):
return f"**{func(*args, **kwargs)}**"
return wrapper
def italic(func):
"""Add italic formatting"""
def wrapper(*args, **kwargs):
return f"*{func(*args, **kwargs)}*"
return wrapper
def underline(func):
"""Add underline formatting"""
def wrapper(*args, **kwargs):
return f"__{func(*args, **kwargs)}__"
return wrapper
def repeat(n):
"""Repeat the output n times"""
def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return " ".join([result] * n)
return wrapper
return decorator
# ============================================================
# CHAINING DECORATORS
# ============================================================
print("\n1. CHAINING DECORATORS")
@bold
@italic
def greet(name):
return f"Hello {name}!"
print(f" {greet('Alice')}")
@underline
@bold
@italic
def farewell(name):
return f"Goodbye {name}!"
print(f" {farewell('Bob')}")
# ============================================================
# ORDER MATTERS
# ============================================================
print("\n2. ORDER MATTERS")
@bold
@italic
def get_message1(name):
return f"Hi {name}!"
@italic
@bold
def get_message2(name):
return f"Hi {name}!"
print(f" bold(italic): {get_message1('Alice')}")
print(f" italic(bold): {get_message2('Alice')}")
# ============================================================
# DECORATORS WITH ARGUMENTS CHAINED
# ============================================================
print("\n3. DECORATORS WITH ARGUMENTS CHAINED")
@repeat(3)
@bold
@italic
def announcement(msg):
return f"ANNOUNCEMENT: {msg}"
print(f" {announcement('Welcome to Python!')}")
# ============================================================
# HOW CHAINING WORKS
# ============================================================
print("\n4. HOW CHAINING WORKS")
print("""
┌─────────────────────────────────────────────────────────────────────┐
│ HOW CHAINING WORKS — Bottom to Top │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ @decorator1 │
│ @decorator2 │
│ @decorator3 │
│ def my_function(): │
│ pass │
│ │
│ Is equivalent to: │
│ │
│ my_function = decorator1(decorator2(decorator3(my_function))) │
│ │
│ decorator3 runs first (closest to the function) │
│ decorator2 runs second │
│ decorator1 runs last (farthest from the function) │
└─────────────────────────────────────────────────────────────────────┘
""")
Chaining decorators key points:
- Multiple decorators — use many decorators on one function
- Bottom to top — the closest decorator to the function runs first
- Order matters — different orders give different results
- Pattern —
@outer @middle @innerruns inner first
Quick Check: Which decorator runs first when chaining? (Answer: The one closest to the function definition — bottom one)
Real-World Example
Building a Web API Handler
# Real-World Example: Web API Handler
import time
import json
from datetime import datetime
print("=" * 60)
print("WEB API HANDLER — DECORATORS IN ACTION")
print("=" * 60)
# ============================================================
# DECORATORS FOR WEB API
# ============================================================
def log_request(func):
"""Log every API request"""
def wrapper(*args, **kwargs):
print(f" [{datetime.now().strftime('%H:%M:%S')}] {func.__name__} called")
print(f" Args: {args}")
result = func(*args, **kwargs)
print(f" Response: {result[:50]}...")
return result
return wrapper
def authenticate(func):
"""Check if user is authenticated"""
def wrapper(user, *args, **kwargs):
if not user.get("authenticated", False):
return {"error": "Authentication required", "status": 401}
return func(user, *args, **kwargs)
return wrapper
def rate_limit(limit_per_minute=60):
"""Rate limit API calls"""
calls = []
def decorator(func):
def wrapper(*args, **kwargs):
now = time.time()
# Remove calls older than 1 minute
calls[:] = [t for t in calls if now - t < 60]
if len(calls) >= limit_per_minute:
return {"error": "Rate limit exceeded", "status": 429}
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
def cache_result(func):
"""Cache API results for 5 seconds"""
cache = {}
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key in cache:
timestamp, result = cache[key]
if time.time() - timestamp < 5:
print(f" Cache hit for {func.__name__}")
return result
result = func(*args, **kwargs)
cache[key] = (time.time(), result)
print(f" Cache miss for {func.__name__}")
return result
return wrapper
# ============================================================
# API HANDLERS WITH DECORATORS
# ============================================================
print("\n1. API HANDLERS")
@log_request
@authenticate
@rate_limit(limit_per_minute=10)
def get_user_profile(user, user_id):
"""Get a user's profile"""
return {"name": "Alice", "email": "alice@example.com", "id": user_id}
@log_request
@authenticate
@cache_result
def get_expensive_data(user, query):
"""Get expensive data with caching"""
time.sleep(0.5) # Simulate expensive operation
return {"data": [1, 2, 3, 4, 5], "query": query}
@log_request
def public_endpoint():
"""Public endpoint — no authentication needed"""
return {"message": "Welcome to the API!"}
# ============================================================
# TESTING THE API
# ============================================================
print("\n2. TESTING API ENDPOINTS")
# Test user
user1 = {"username": "alice", "authenticated": True}
user2 = {"username": "bob", "authenticated": False}
print("\n --- Get User Profile (authenticated) ---")
result = get_user_profile(user1, 123)
print(f" Result: {result}")
print("\n --- Get User Profile (not authenticated) ---")
result = get_user_profile(user2, 123)
print(f" Result: {result}")
print("\n --- Get Expensive Data (first call — cache miss) ---")
result = get_expensive_data(user1, "python")
print(f" Result: {result}")
print("\n --- Get Expensive Data (second call — cache hit) ---")
result = get_expensive_data(user1, "python")
print(f" Result: {result}")
print("\n --- Public Endpoint ---")
result = public_endpoint()
print(f" Result: {result}")
print("\n --- Rate Limit Test ---")
for i in range(12):
result = get_user_profile(user1, i)
if isinstance(result, dict) and result.get("error") == "Rate limit exceeded":
print(f" Rate limit hit after {i} calls")
break
# ============================================================
# DECORATOR PATTERNS SUMMARY
# ============================================================
print("\n" + "=" * 60)
print("DECORATOR PATTERNS SUMMARY")
print("=" * 60)
print("""
┌─────────────────────┬──────────────────────────────────────────────────┐
│ DECORATOR │ PURPOSE │
├─────────────────────┼──────────────────────────────────────────────────┤
│ log_request │ Log every API call for debugging │
│ authenticate │ Check user authentication before accessing │
│ rate_limit │ Prevent API abuse by limiting requests │
│ cache_result │ Store results to speed up repeated queries │
└─────────────────────┴──────────────────────────────────────────────────┘
Real-world decorators you might use:
• @login_required — from Flask/Django
• @app.route — from Flask for URL routing
• @cache — from functools
• @property — Python's built-in property decorator
""")
Real-world example key points:
- log_request — logs every API request
- authenticate — checks if user is logged in
- rate_limit — prevents API abuse
- cache_result — speeds up repeated requests
- Chained decorators — multiple decorators work together
Quick Check: What's the purpose of the cache_result decorator? (Answer: It stores results to avoid running expensive operations multiple times)
Best Practices
Using Decorators Effectively
# Best Practices for Decorators
print("=" * 60)
print("BEST PRACTICES FOR DECORATORS")
print("=" * 60)
# ============================================================
# 1. USE functools.wraps
# ============================================================
print("\n1. USE functools.wraps")
# BAD: Without wraps — loses function metadata
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def my_function():
"""This is my function"""
pass
print(f" Without wraps:")
print(f" Name: {my_function.__name__}") # Shows 'wrapper', not 'my_function'
print(f" Docstring: {my_function.__doc__}") # Shows None
# GOOD: With wraps — preserves metadata
from functools import wraps
def good_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def my_good_function():
"""This is my good function"""
pass
print(f"\n With wraps:")
print(f" Name: {my_good_function.__name__}")
print(f" Docstring: {my_good_function.__doc__}")
# ============================================================
# 2. USE *args AND **kwargs
# ============================================================
print("\n2. USE *args AND **kwargs")
def flexible_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f" Calling with args: {args}, kwargs: {kwargs}")
return func(*args, **kwargs)
return wrapper
@flexible_decorator
def add(a, b):
return a + b
@flexible_decorator
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(f" add(5, 3): {add(5, 3)}")
print(f" greet('Alice', greeting='Hi'): {greet('Alice', greeting='Hi')}")
# ============================================================
# 3. KEEP DECORATORS SIMPLE
# ============================================================
print("\n3. KEEP DECORATORS SIMPLE")
# GOOD: Simple, focused decorator
def timer_simple(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f" {func.__name__} took {time.time() - start:.3f}s")
return result
return wrapper
# BAD: Decorator doing too much
def over_complicated(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Timing, logging, caching, retry, authentication...
# Too much! Split into multiple decorators
pass
return wrapper
# ============================================================
# 4. USE DECORATOR FACTORIES FOR CONFIGURATION
# ============================================================
print("\n4. USE DECORATOR FACTORIES FOR CONFIGURATION")
def repeat_n_times(n):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(n):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
@repeat_n_times(3)
def say_hi(name):
return f"Hi {name}!"
print(f" {say_hi('Alice')}")
# ============================================================
# 5. DOCUMENT YOUR DECORATORS
# ============================================================
print("\n5. DOCUMENT YOUR DECORATORS")
def documented_decorator(func):
"""
This decorator does XYZ.
Usage: @documented_decorator
"""
@wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper function that does ABC"""
return func(*args, **kwargs)
return wrapper
# ============================================================
# 6. SUMMARY
# ============================================================
print("\n" + "=" * 60)
print("BEST PRACTICES SUMMARY")
print("=" * 60)
print("""
┌─────────────────────────────┬─────────────────────────────────────────────┐
│ PRACTICE │ WHY IT MATTERS │
├─────────────────────────────┼─────────────────────────────────────────────┤
│ Use functools.wraps │ Preserves function name, docstring, etc │
│ │ │
│ Use *args and **kwargs │ Works with any function signature │
│ │ │
│ Keep decorators simple │ Easy to understand and maintain │
│ │ │
│ Use decorator factories │ Customize behavior with arguments │
│ │ │
│ Document your decorators │ Users know how to use them │
└─────────────────────────────┴─────────────────────────────────────────────┘
REMEMBER:
• Always use @wraps to preserve function metadata
• Use *args and **kwargs for flexibility
• One decorator = one job
• Chain decorators for multiple behaviors
""")
Best practices summary:
- Use functools.wraps — preserves function metadata
- Use *args and **kwargs — makes decorators flexible
- Keep decorators simple — one decorator = one job
- Use decorator factories — for customization with arguments
- Document decorators — tell users how to use them
Quick Check: Why should you use @wraps in decorators? (Answer: It preserves the original function's name, docstring, and other metadata)
Try It Yourself
Experiment with decorators in the editor below.
DECORATORS - PRACTICE
==================================================
1. BASIC DECORATOR
🔍 Calling multiply
🔍 Args: (4, 5), {}
🔍 Result: 20
20
🔍 Calling greet
🔍 Args: ('Alice',), {'greeting': 'Hi'}
🔍 Result: Hi, Alice!
Hi, Alice!
2. DECORATOR WITH ARGUMENTS
say_hello('Bob') repeated 3 times:
['Hello Bob!', 'Hello Bob!', 'Hello Bob!']
3. CHAINING DECORATORS
shout_message('hello'): HELLO!!!
4. TIMING DECORATOR
count_to_n took 0.0234s
Result: 499999500000
You've Got It!
You now understand decorators in Python. You know how to create custom decorators, use decorator factories, and chain decorators together.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is a decorator in Python?
What does @ do in Python?
@my_decorator above a function is the same as writing my_function = my_decorator(my_function). It makes decorators easier to read and use.
Why use @wraps in decorators?
@wraps preserves the original function's metadata like its name, docstring, and annotations. Without it, the decorated function would lose its identity, making debugging and documentation harder.
Can I use multiple decorators on one function?
Can I pass arguments to a decorator?
What are some common uses of decorators?
Where to Go From Here
Now that you understand decorators in Python, check out these related topics:
Property Decorator
Learn about Python's built-in @property decorator for getters and setters.
Learn More →Generators
Learn about generators — another powerful Python feature.
Learn More →Context Managers
Learn how context managers work and how to create them.
Learn More →