Python decorators are one of the most powerful and expressive features the language offers. Despite their intimidating name, decorators are conceptually straightforward and incredibly practical for writing cleaner, more readable, and reusable code. They allow you to modify or enhance the behavior of functions or methods without permanently changing their source code.
In this lesson, we'll explore decorators from the ground up. By the end, you'll understand what decorators are, how to create your own, and when to apply them effectively in your Python projects.
What Is a Decorator?
At its core, a decorator is a function that takes another function as an argument and returns a new function that enhances or modifies the original one. This might sound abstract, so let's break it down.
Imagine you have a simple function that greets a user:
📌 Deep Dive: Basic Function
def greet(name):
return f"Hello, {name}!"
This function simply returns a greeting. Now, what if you want to add extra behavior — for example, logging whenever the function is called — without modifying the greet function's code directly? This is where a decorator shines.
Functions as First-Class Objects
Before jumping into decorators, it's essential to understand that in Python, functions are first-class objects. This means:
- You can assign functions to variables.
- You can pass functions as arguments to other functions.
- You can return functions from functions.
This flexibility enables decorators to exist.
📌 Deep Dive: Passing Functions
def shout(text):
return text.upper()
def whisper(text):
return text.lower()
def speak(style, message):
return style(message)
print(speak(shout, "Hello"))
print(speak(whisper, "Hello"))
Here, speak takes a function (style) as an argument and calls it with a message. This pattern underpins how decorators work.
Creating Your First Decorator
Let's write a simple decorator that logs the execution of a function:
📌 Deep Dive: Logging Decorator
def log_decorator(func):
def wrapper(*args, **kwargs):
print(f"Calling function: {func.__name__}")
result = func(*args, **kwargs)
print(f"Function {func.__name__} returned {result}")
return result
return wrapper
@log_decorator
def add(a, b):
return a + b
add(3, 5)
Let's analyze what's happening:
log_decoratoris a function that accepts another functionfunc.- Inside, it defines
wrapper, which wraps the original function call with extra print statements. wrapperuses*argsand**kwargsto accept any number of positional and keyword arguments, ensuring flexibility.- The original function is called inside
wrapperand its result is returned. log_decoratorreturns thewrapperfunction, effectively replacing the original function with this enhanced version.- The
@log_decoratorsyntax is a shorthand foradd = log_decorator(add).
Decorator Syntax and How It Works
The @ symbol before a function name is syntactic sugar to apply a decorator. For example:
@decoratorabove a function is equivalent to rewriting the function asfunction = decorator(function).- This means the original function is replaced by the decorated version returned by the decorator.
Without the @ syntax, the previous example would look like this:
📌 Deep Dive: Manual Decorator Application
def add(a, b):
return a + b
add = log_decorator(add)
add(3, 5)
Both approaches achieve the same result, but the @ syntax is cleaner and easier to read.
Why Use Decorators?
Decorators help you:
- Reuse code: You can apply the same decorator to multiple functions.
- Separate concerns: Keep core logic clean by moving auxiliary tasks like logging, timing, or authentication outside the main function.
- Enhance readability: By reading
@decoratorabove a function, you know immediately what extra behavior is added.
Common Use Cases of Decorators
- Logging function calls and arguments
- Measuring execution time
- Access control / authentication
- Memoization / caching
- Retrying failed operations
Passing Arguments to Decorators
Sometimes you want a decorator that is configurable. For example, you might want to customize the logging message or the number of retries for a retry decorator. To do this, you create a decorator factory — a function that returns a decorator.
📌 Deep Dive: Parameterized Decorator
def repeat(num_times):
def decorator_repeat(func):
def wrapper(*args, **kwargs):
result = None
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator_repeat
@repeat(num_times=3)
def say_hello():
print("Hello!")
say_hello()
Here, repeat is the decorator factory that takes num_times and returns the actual decorator decorator_repeat. This decorator wraps the function so it runs multiple times.
Decorators With Arguments vs Without
| Without Arguments | With Arguments |
|---|---|
| Simple function wrapping another function | Function returning a decorator function |
Defined as def deco(func): ... | Defined as def deco(arg): return actual_decorator |
Used like @deco | Used like @deco(arg) |
Preserving Function Metadata with functools.wraps
When you create a decorator, the wrapper function hides the original function's metadata such as its name, docstring, and annotations. This can be problematic for debugging and introspection.
Python's functools module provides a handy decorator called wraps to solve this. It copies the metadata from the original function to the wrapper.
📌 Deep Dive: Using functools.wraps
import functools
def log_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_decorator
def greet(name):
"""Say hello to someone."""
print(f"Hello, {name}!")
greet("Alice")
print(greet.__name__)
print(greet.__doc__)
Without @functools.wraps, greet.__name__ would return 'wrapper' and the docstring would be lost.
Decorators for Methods in Classes
Decorators work the same way with methods inside classes. However, remember that methods receive self as their first argument.
📌 Deep Dive: Method Decorator
import functools
def log_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
class Calculator:
@log_decorator
def multiply(self, a, b):
return a * b
calc = Calculator()
print(calc.multiply(4, 5))
The decorator treats self like any other argument — passed along seamlessly.
Stacking Multiple Decorators
You can apply more than one decorator to a function by stacking them on top of each other. The decorators are applied from the closest to the function upwards.
📌 Deep Dive: Multiple Decorators
def uppercase(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
def exclaim(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result + "!"
return wrapper
@exclaim
@uppercase
def greet(name):
return f"Hello, {name}"
print(greet("Bob"))
Here, greet is first passed to uppercase, then the result is passed to exclaim. So the output becomes uppercase with an exclamation mark.
Common Pitfalls to Avoid
⚠️ Be Careful with Mutable Default Arguments
When writing decorators that accept parameters, avoid using mutable default arguments like lists or dictionaries, as they can lead to unexpected behavior.
⚠️ Always Use functools.wraps
Neglecting to use functools.wraps can make debugging harder and interfere with tools that rely on function metadata.
⚠️ Mind the Order of Stacked Decorators
The order you stack decorators affects the final behavior. Apply them thoughtfully, especially when they modify output or side effects.
Visualizing Decorator Architecture

Summary and Best Practices
- Decorators are functions that modify other functions, enabling code reuse and separation of concerns.
- Use the
@decoratorsyntax for clarity and brevity. - Always use
functools.wrapsinside your decorators to preserve function metadata. - Support arbitrary arguments with
*argsand**kwargsin wrapper functions. - When needing parameters, create decorator factories (functions returning decorators).
- Be mindful of decorator stacking order and side effects.
💡 Think of Decorators Like Gift Wrappers
A decorator wraps a function with additional "packaging" — like a gift wrapper that adds a decorative layer, making the function more exciting or useful without altering the gift inside.
With this solid foundation, you can explore Python's built-in decorators such as @staticmethod, @classmethod, and @property, or even create complex custom decorators tailored to your projects.
Quick Knowledge Check
Test what you just learned
Question 1 of 2
What does a Python decorator primarily do?
Question 2 of 2
Which Python module and decorator help preserve the original function’s metadata when creating decorators?
Loading results...