Of course! Let's break down Python decorators from the ground up. They are a powerful and elegant feature in Python, but they can seem tricky at first.

The Core Idea: Functions as First-Class Citizens
To understand decorators, you first need to understand that in Python, functions are "first-class citizens." This means you can:
- Assign a function to a variable.
- Pass a function as an argument to another function.
- Return a function from another function.
This last point is the key to decorators.
def say_hello():
print("Hello!")
# 1. Assigning a function to a variable
greet = say_hello
greet() # Output: Hello!
# 2. Passing a function as an argument
def run_function(func):
print("Running the function you passed in:")
func()
run_function(say_hello) # Output: Running the function you passed in: \n Hello!
# 3. Returning a function from another function
def create_multiplier(n):
def multiplier(x):
return x * n
return multiplier
times_3 = create_multiplier(3)
print(times_3(10)) # Output: 30
What is a Decorator?
A decorator is a function that takes another function as an argument (the "decorated" function), adds some kind of functionality, and then returns a new function.
It's essentially a wrapper that lets you extend the behavior of a function without permanently modifying it.

Let's build a simple one. Imagine we want to print a message before and after another function runs.
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func() # Call the original function
print("Something is happening after the function is called.")
return wrapper
def say_whee():
print("Whee!")
# Now, let's decorate say_whee
say_whee = my_decorator(say_whee)
# Call the decorated function
say_whee()
Output:
Something is happening before the function is called.
Whee!
Something is happening after the function is called.
What happened?
- We defined
say_whee. - We passed
say_wheetomy_decorator. my_decoratorreturned thewrapperfunction.- We assigned this returned
wrapperfunction back to thesay_wheevariable. - Now, when we call
say_whee(), we are actually calling thewrapper()function, which contains the extra logic.
The Syntax (Syntactic Sugar)
Manually reassigning the function like say_whee = my_decorator(say_whee) is a bit clunky. Python provides a much cleaner way to do this using the symbol.
This is called the "pie" syntax. It does the exact same thing as the manual assignment.
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
@my_decorator
def say_whee():
print("Whee!")
say_whee()
The output is identical. The @my_decorator line above the function definition is just a shortcut for say_whee = my_decorator(say_whee).
Decorators with Arguments
What if the function we want to decorate takes arguments? For example, say_hello(name).
Our current wrapper function doesn't take any arguments. We need to make it more flexible. We can use *args and **kwargs to accept any number of positional and keyword arguments.
def do_twice(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs) # Call the original function with its arguments
func(*args, **kwargs) # Call it a second time
return wrapper
@do_twice
def greet(name):
print(f"Hello, {name}")
greet("Alice")
Output:
Hello, Alice
Hello, Alice
Here, *args captures "Alice" and **kwargs would capture any keyword arguments (like greet(name="Alice")).
Preserving Metadata with functools.wraps
When you decorate a function, it "forgets" its original name and docstring. This can be problematic for debugging and documentation.
def do_twice(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper
@do_twice
def greet(name):
"""This function greets a person."""
print(f"Hello, {name}")
print(greet.__name__) # Output: wrapper
print(greet.__doc__) # Output: None
To fix this, use the @functools.wraps decorator. It's a decorator that you apply to your wrapper function to "copy" the metadata from the original function.
import functools
def do_twice(func):
@functools.wraps(func) # <-- Add this!
def wrapper(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper
@do_twice
def greet(name):
"""This function greets a person."""
print(f"Hello, {name}")
print(greet.__name__) # Output: greet
print(greet.__doc__) # Output: This function greets a person.
Best Practice: Always use @functools.wraps when you write your own decorators.
Practical Examples
Example 1: Timing a Function
A classic use case for decorators is performance monitoring.
import time
import functools
def timer(func):
"""Print the runtime of the decorated function."""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value # Return the result of the original function
return wrapper_timer
@timer
def waste_some_time(num_times):
for _ in range(num_times):
sum(range(num_times))
waste_some_time(10000)
Output:
Finished 'waste_some_time' in 0.0123 secs
Example 2: Checking for User Permissions
def admin_required(func):
"""Only allows admin users to access the decorated function."""
@functools.wraps(func)
def wrapper_user_check(user, *args, **kwargs):
if user.get('role') != 'admin':
raise PermissionError("User does not have admin privileges.")
return func(user, *args, **kwargs)
return wrapper_user_check
@admin_required
def delete_database(user, db_name):
print(f"Admin {user['name']} is deleting database {db_name}.")
current_user = {'name': 'Alice', 'role': 'user'}
admin_user = {'name': 'Bob', 'role': 'admin'}
# This will fail
try:
delete_database(current_user, 'my_app_db')
except PermissionError as e:
print(e)
# This will succeed
delete_database(admin_user, 'my_app_db')
Output:
User does not have admin privileges.
Admin Bob is deleting database my_app_db.
Decorators with Arguments
What if you want to pass arguments to the decorator itself? For example, @repeat(num_times=3).
This requires an extra layer of nesting. The structure becomes:
decorator_with_args(arg1, arg2)(func)
- The outermost function (
repeat) takes the arguments. - It returns a new decorator (
actual_decorator). - This new decorator then takes the function (
func) and returns thewrapper.
import functools
def repeat(num_times):
def decorator_repeat(func):
@functools.wraps(func)
def wrapper_repeat(*args, **kwargs):
for _ in range(num_times):
value = func(*args, **kwargs)
return value
return wrapper_repeat
return decorator_repeat
@repeat(num_times=4)
def greet(name):
print(f"Hello {name}")
greet("World")
Output:
Hello World
Hello World
Hello World
Hello World
Summary
| Concept | Description | Example |
|---|---|---|
| First-Class Functions | Functions can be passed as arguments, returned from other functions, and assigned to variables. | def f(): pass; g = f |
| Basic Decorator | A function that wraps another function to add behavior. | def my_decorator(func): ... |
| Syntax | A clean, syntactic shortcut for applying a decorator. | @my_decorator def f(): ... |
| *`args, kwargs` | Makes a decorator work with functions that take any arguments. | def wrapper(*args, **kwargs): ... |
functools.wraps |
Preserves the original function's name, docstring, and other metadata. | @functools.wraps(func) |
| Decorator with Args | A decorator factory that takes arguments. Requires an extra layer of nesting. | def repeat(n): def decorator(func): ... |
