What Is a Decorator in Python?
Imagine you have a perfectly wrapped gift. Inside, there is a toy (your function). Now, imagine you want to add a card and a bow to it without opening the box or changing the toy itself. You simply wrap the gift with a new layer.
In Python, a decorator does exactly this. It takes a function and "wraps" it with another function. The wrapper runs code before and/or after your original function runs.
The Core Idea
You are not changing the original function's code. You are enhancing its execution context.
How Wrapping Changes Behavior
The wrapper completely replaces the original function name, but inside the wrapper, you still have access to the original function. This allows you to:
- Run code before the function executes (e.g., logging, validation).
- Run code after it finishes (e.g., cleanup, notifications).
- Modify inputs before they reach the function.
- Modify outputs after the function returns.
- Keep track of state (data) across multiple calls.
1. Altering Arguments
The wrapper can change inputs before passing them along.
def force_uppercase(func):
def wrapper(name):
# Change argument before calling original
return func(name.upper())
return wrapper
@force_uppercase
def greet(name):
print(f"Hello, {name}!")
greet("alice") # Prints: Hello, ALICE!
2. Maintaining State
The wrapper can remember data between calls using a closure.
def count_calls(func):
count = 0 # Lives in the closure
def wrapper(*args, **kwargs):
nonlocal count
count += 1
print(f"Call #{count}")
return func(*args, **kwargs)
return wrapper
Decorator Visualizer
How to Use Decorators in Python
Think of decorators as a way to extract secondary responsibilities from your functions. Your function's job is to do one thing well—calculate a result, transform data, make a decision. Anything that's not that core job—logging, timing, access control—can be moved into a decorator.
This keeps your core functions clean, readable, and reusable. They don't contain boilerplate code that's unrelated to their primary purpose.
The "Clean vs. Messy" Comparison
❌ Without Decorator
# Core logic is mixed with logging
def process_data(data):
print(f"Starting... {len(data)} items") # secondary
result = [d * 2 for d in data] # core
print(f"Finished...") # secondary
return result
✅ With Decorator
@log_execution # handles printing
def process_data(data):
# pure transformation logic only
return [d * 2 for d in data]
The Trap: Overusing Decorators
While decorators are powerful, there is a trap. Stacking too many decorators leads to hidden side effects. When you see a function wrapped in four layers, you can't understand it by reading the body alone.
Debugging Nightmare
When something breaks, you aren't debugging the function. You are debugging the interaction between the wrappers. Did the cache return a stale value? Did the authenticator silently fail?
The "Stack" Visualizer
Basic Syntax of a Python Decorator
The @ symbol often looks like magic, but it's actually just syntactic sugar. It's a convenient shorthand that Python provides to save you from typing a few extra lines.
When you place @my_decorator above a function, Python is simply telling you: "Take this function you just defined, pass it into the decorator, and replace the function name with whatever the decorator returns."
The "Under the Hood" Transformation
This is what Python actually does when it sees the @ symbol:
def my_function(): pass
my_function = my_decorator(my_function)
How the Wrapper Function Is Built
A decorator is simply a higher-order function—a function that takes another function as an argument and returns a new function.
def log_decorator(func): # 1. Takes function
def wrapper(): # 2. Defines inner function
print("Calling...")
func() # 3. Calls original
return wrapper # 4. Returns wrapper
@log_decorator
def say_hello():
print("Hello!")
Key Insight: The @ syntax just automates the reassignment. Without it, you'd have to manually type say_hello = log_decorator(say_hello).
Syntax Transformation
Function Decorators in Python
Let's get practical. A very common use for decorators is measuring execution time. You want to know how long a function takes, but you don't want to clutter your function's code with timer logic.
The decorator acts like a stopwatch. It starts the timer, runs your function, stops the timer, and prints the result. Your function remains pure and focused on its actual job.
Building a Timing Decorator
Here is how we construct a decorator that wraps a function to measure its duration:
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.perf_counter() # 1. Start clock
result = func(*args, **kwargs) # 2. Run original function
end = time.perf_counter() # 3. Stop clock
print(f"{func.__name__} took {end - start:.4f}s")
return result # 4. Return result
return wrapper
Why *args, **kwargs? It allows the wrapper to accept any arguments, making it reusable for any function signature.
The "Replacement" Misconception
Beginners often think a decorator must call the original function. This is false. A decorator simply replaces the function name with whatever callable it returns.
The returned object can call the original function, modify its result, or ignore it completely.
Example: The "Ignore" Decorator
This decorator returns a function that always returns 42, never even running the original code.
def always_forty_two(func):
def wrapper(*args, **kwargs):
return 42 # Original function is ignored!
return wrapper
@always_forty_two
def compute_something(x):
return x * 2
print(compute_something(10)) # Prints 42, NOT 20
Key Insight: You are not just "wrapping" logic; you are reassigning the name. The name `compute_something` now points to `wrapper`, which has no idea what `compute_something` used to do.
The Function Replacer
Decorating Methods and Classes
When you decorate a method inside a class, you introduce a new player: self.
Methods are special because Python automatically passes the instance (the object) as the first argument. If your decorator's wrapper doesn't know how to catch that first argument, the code crashes.
The "Missing Key" Problem
Think of self as a key required to open the door to the object's data. A naive decorator might close the door before the key arrives.
❌ The Trap (Strict Wrapper)
def bad_decorator(func):
def wrapper(): # <--- Problem! No args defined
return func()
return wrapper
@bad_decorator
def greet(self): # Expects 'self' key
pass
# Error: greet() missing 1 required positional argument: 'self'
✅ The Fix (Flexible Wrapper)
def good_decorator(func):
def wrapper(*args, **kwargs): # <--- Accepts everything
return func(*args, **kwargs) # Passes everything through
return wrapper
Preserving Identity with functools.wraps
Even with the fix, there is a subtle side effect. When you wrap a function, the original name is lost. The decorated method is technically just named wrapper.
This confuses debuggers and documentation tools. To fix this, Python provides @functools.wraps(func). It copies the original function's name and docstring into the wrapper, making the decoration "transparent."
Method Argument Relay
Python Decorator Tutorial: Parameterized Decorators
So far, our decorators have been fixed—they always do the same thing. But what if you want to configure a decorator? For example, a @repeat that runs a function 5 times, or a @timer that uses milliseconds instead of seconds?
This requires a Parameterized Decorator. Think of it as a Decorator Factory. Instead of wrapping the function directly, you first create a factory function that accepts your parameters (like `times=5`) and returns the actual decorator.
The "Factory" Pattern
When you write @repeat(times=3), Python actually does this behind the scenes:
# 1. The Factory Function
def repeat(times):
# 2. The Actual Decorator (returned by factory)
def decorator(func):
# 3. The Wrapper (returned by decorator)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
Key Insight: Notice the three layers? The outer repeat remembers times via a closure, passing it down to the wrapper.
Common Misconception: "It's Too Complex"
Beginners often panic seeing three nested functions. But don't let the extra layer fool you.
The Gift Wrap Analogy
Simple Decorator: You hand a gift to a wrapper. It wraps it. Done.
Parameterized Decorator: You go to a Gift Wrap Station (the Factory). You choose the paper color (parameter). The station prepares a custom wrapper for you, which you then use to wrap the gift.
Factory Visualizer
Common Misconceptions and Pitfalls
1. The Thread Safety Trap
When a decorator holds mutable state (like a counter or cache), that state lives in the closure. If multiple threads call the function simultaneously, they share that same closure. Without protection, they might read, modify, and write the variable at the exact same time, causing **race conditions**.
For example, two threads might both read a counter as `5`, increment it to `6`, and write it back. One increment is silently lost.
The Solution: Locks
You must protect shared state with a lock to ensure atomicity.
import threading
def count_calls_threadsafe(func):
count = 0
lock = threading.Lock()
def wrapper(*args, **kwargs):
nonlocal count
with lock: # Only one thread at a time
count += 1
current = count
print(f"Call #{current} to {func.__name__}")
return func(*args, **kwargs)
return wrapper
2. The "Identity Crisis" (Metadata)
When you wrap a function, the wrapper replaces the original. By default, the wrapper's metadata—its __name__, __doc__, and annotations—hides the original's.
This breaks debugging tools and makes help() useless. The solution is @functools.wraps.
Why It Matters
Without @functools.wraps, your documentation tools will list your function as "wrapper" instead of "calculate_total".
import functools
def log_decorator(func):
@functools.wraps(func) # <--- This line is essential
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}...")
return func(*args, **kwargs)
return wrapper
The Identity Crisis Visualizer
Practical Examples in Real Projects
In real-world software, decorators shine when handling cross-cutting concerns. These are tasks that affect many parts of your application but aren't the core business logic.
The two most common examples are Logging (tracking what happens) and Authentication (controlling access). By using decorators, we keep these "side tasks" separate from the function's actual job.
Building a Practical Logger
Here is a decorator that timestamps calls and logs arguments. This is far more useful than a simple print because it provides context for debugging.
import functools, datetime
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 1. Capture context
ts = datetime.datetime.now().isoformat()
args_str = ", ".join([repr(a) for a in args])
print(f"[{ts}] CALL {func.__name__}({args_str})")
# 2. Execute
result = func(*args, **kwargs)
# 3. Log result
print(f"[{ts}] RETURN {result}")
return result
return wrapper
Why this works well:
functools.wraps: Keeps the function's name and help text intact.*args, **kwargs: Works with any function signature.- Timestamps: Essential for tracing execution order in complex systems.
The Trap: "Decorators Replace Everything"
It's tempting to stack decorators for every feature: @auth, @rate_limit, @validate.
While this works for small scripts, it creates a hidden complexity in larger apps. If you need to reuse that same logic for a CLI command or a background job, you can't easily reuse the stack.
Better Architecture
Use decorators for simple, reusable wrappers. For complex pipelines (like Auth + Rate Limit + Validation), use Middleware or a base class. Decorators should complement your architecture, not replace it.
The "Stacking" Problem
Frequently Asked Questions (FAQ)
Decorators are powerful, but they come with a few quirks. Let's clear up the most common questions and confusion points.
What is a Decorator?
A decorator is simply a function that takes another function as input and returns a new function (the wrapper).
The @ syntax is just shorthand.
def func(): ...
# Is equivalent to:
func = my_decorator(func)
Why is my function returning None?
This is the #1 mistake. If your wrapper doesn't explicitly return the result of the original function, it returns None.
# WRONG
def wrapper(func):
func() # Result is calculated but discarded!
# RIGHT
def wrapper(func):
return func() # Pass the result back up
Can I use Decorators on Classes?
Yes! But remember that methods need self.
- Always use
*args, **kwargsin your wrapper to catchself. - For
@classmethod, apply your decorator before the classmethod decorator. - Use
@functools.wrapsto keep your function name visible in error logs.