Python Decorators Explained: Examples and Use Cases
Learn how Python decorators transform functions, how @ syntax and stacking work, and how to write reusable wrappers and configured decorators.

A Python decorator is a callable that transforms a function, method, or class definition. The familiar @decorator syntax applies that transformation where the definition is declared. A wrapper decorator usually adds behavior before or after calling the original function; other decorators can register or otherwise transform a definition without wrapping each call.
For example, @announce above a function is equivalent to defining the function and then writing greet = announce(greet). When a decorator returns a wrapper, later calls to greet call that wrapper. Use functools.wraps to preserve useful metadata, and use a decorator factory when you need configuration such as a retry count or cache size.
1. What does a Python decorator do?
A decorator receives a definition as an input and returns a value that is bound to that definition’s name. For a function decorator, the input is usually a function object. The decorator might return a wrapper function, the same function after registering it somewhere, or another transformed object.
Python’s decorator syntax makes this transformation visible next to the definition. PEP 318 specifies that:
@dec
def func():
pass
is equivalent in effect to:
def func():
pass
func = dec(func)
The space before def in the first snippet is not part of valid top-level Python indentation; the valid decorated form is @dec on one line followed immediately by an unindented def. The equivalence is useful for understanding when the decorator runs: Python evaluates the decorator expression and applies it as the function definition is executed, such as when a module is imported. The decorator is not automatically rerun every time the decorated function is called. If it returns a wrapper, that wrapper’s body runs on each call.
Primary reference: PEP 318: Decorators for Functions and Methods.
2. Write a basic wrapper decorator
A wrapper decorator has three parts: a decorator that accepts the original function, an inner wrapper that accepts the original call’s arguments, and a return statement that gives the wrapper back to Python.

from functools import wraps
def announce(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Finished {func.__name__}")
return result
return wrapper
@announce
def greet(name):
return f"Hello, {name}!"
if __name__ == "__main__":
message = greet("Ada")
print(message)
Save it as decorator_example.py and run python decorator_example.py. It prints a message before the call, one after it, and then the returned greeting. The wrapper uses *args and **kwargs so it can forward positional and keyword arguments to many kinds of functions. Returning result matters: without it, the decorated function would return None even when the original function produced a value.
The decorator executes once when Python processes the decorated definition. In this example, it creates and returns wrapper. Each subsequent call to greet invokes that wrapper, which then invokes the original function.
3. Why use functools.wraps?
Without @wraps(func), the decorated name refers to wrapper, so introspection may show the wrapper’s name, documentation, annotations, and other attributes instead of the original function’s. functools.wraps is intended for this wrapper pattern. It uses update_wrapper to copy selected attributes from the wrapped callable and update the wrapper’s attribute dictionary. Read the version-specific details in the Python functools documentation.
Metadata preservation helps documentation tools, debuggers, and code that inspects functions. The wrapper remains a different callable, but attributes such as its displayed name and docstring point back to the wrapped function. In current Python versions, inspect.signature can also follow the __wrapped__ reference that wraps sets.
from functools import wraps
def trace(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@trace
def add(left: int, right: int) -> int:
"""Return the sum of two integers."""
return left + right
print(add.__name__) # add
print(add.__doc__) # Return the sum of two integers.
print(add(2, 3)) # 5
4. Decorators that accept configuration
A decorator factory is a function that takes configuration and returns a decorator. There are three distinct stages: the factory receives options, the returned decorator receives the function, and the wrapper receives runtime call arguments.

from functools import wraps
def repeat(times):
if times < 1:
raise ValueError("times must be at least 1")
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorate
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
return name
if __name__ == "__main__":
greet("Ada")
@repeat(3) first calls repeat(3), which returns decorate. Python then calls decorate(greet). The resulting wrapper repeats the original call three times and returns the final result. This example repeats side effects as well as computation, so use it only when that behavior is intended.
5. Stacking decorators and understanding order
With multiple decorators, Python applies the one closest to the function first. The result is then passed to the decorator above it. Thus @outer over @inner means outer(inner(func)), not inner(outer(func)).
@outer
@inner
def task():
return "done"
# Equivalent rebinding:
task = outer(inner(task))
This order affects both setup and runtime behavior. The inner decorator first transforms the original function. The outer decorator receives that transformed result. If both return wrappers, calling the final function enters the outer wrapper before the inner one, then returns through them in reverse order.
When order is not obvious, temporarily expand the decorators into ordinary assignments or give each layer a descriptive name. Be especially careful when one decorator changes the callable’s arguments or return type, and another expects the original contract.
6. Common decorator uses
Decorators are useful when behavior should be applied consistently to several definitions and the behavior belongs next to each definition. Common patterns include:
- Method binding: built-ins such as
@classmethodand@staticmethodtransform methods’ binding behavior. - Registration: a decorator can add a function to a registry or mark it for later use; it need not wrap every call.
- Instrumentation: logging, timing, tracing, or metrics can be added around calls.
- Access checks: a wrapper can verify permissions or preconditions before calling the original function.
- Caching:
functools.cacheandfunctools.lru_cacheprovide standard memoization decorators for suitable functions. - Class transformation: class decorators can transform or register a class definition.
Prefer a decorator when the shared behavior is clear at the call site and the wrapper’s impact is easy to understand. A direct helper call or explicit code can be easier to read when the behavior is used once, alters control flow substantially, or hides important side effects.
7. A practical design checklist
- Decide what is being transformed. Is it a function, method, or class? Does the decorator wrap calls, register the definition, or produce a different object?
- Separate setup from runtime. Put decorator options in a factory. Keep per-call inputs in the wrapper’s
*argsand**kwargs. - Preserve the contract. Forward arguments, return the original result, and allow exceptions to propagate unless changing that behavior is intentional.
- Use
@wraps. Apply it to the wrapper with the function being wrapped as its argument. - Check side effects. A retry or repeat decorator can execute writes, payments, messages, or other effects more than once.
- Check composition. Write down stacked decorator expansion when order influences behavior.
- Keep configuration explicit. Validate invalid options when the factory runs so errors appear near the decorated declaration.
8. Troubleshooting decorator bugs
| Symptom | Likely cause | Fix |
|---|---|---|
The decorated function returns None |
The wrapper calls the original but omits return. |
Return the original result, or explicitly document a changed return contract. |
Arguments cause a TypeError |
The wrapper signature is narrower than the function’s call pattern. | Use *args, **kwargs and forward both, or define a deliberate matching signature. |
| Help or logs show “wrapper” | Function metadata was not copied. | Decorate the wrapper with @functools.wraps(func). |
| A configured decorator reports a missing argument | The factory and decorator layers were confused, such as writing @repeat when the API expects @repeat(3). |
Match the intended form: a plain decorator takes the function; a factory takes options and returns a decorator. |
| Calls execute in an unexpected order | Stacking order was assumed top-to-bottom. | Expand to name = outer(inner(name)); the decorator nearest the function is applied first. |
| Async work is not awaited | A normal wrapper returned a coroutine without awaiting it, or changed the function kind. | For async functions, define async def wrapper(...) and await the original coroutine when the wrapper needs to inspect its result. |
| Repeated calls produce duplicate side effects | A retry/repeat wrapper reruns a non-idempotent operation. | Use retries only with a safe operation or add an idempotency strategy at the application boundary. |
9. Async functions and methods
A synchronous wrapper can call an async function, but it receives a coroutine object. If it needs the function’s completed result, use an asynchronous wrapper and await that coroutine:
from functools import wraps
def async_trace(func):
@wraps(func)
async def wrapper(*args, **kwargs):
print(f"Starting {func.__name__}")
result = await func(*args, **kwargs)
print(f"Finished {func.__name__}")
return result
return wrapper
Keep the wrapper’s async nature consistent with the wrapped function’s expected use. A decorator that supports both sync and async callables needs to detect or deliberately distinguish those cases; do not assume one generic wrapper preserves both calling conventions.
10. Performance, reliability, and cost
A wrapper adds a function call and whatever work its body performs. For I/O-bound work, logging or validation may be small compared with the operation; in very hot CPU-bound paths, extra wrapper layers can matter. Keep wrappers short and avoid repeated expensive setup inside the call path when it can be done once at decoration time.
Reliability depends on preserving the wrapped function’s contract. Be clear about exceptions, return values, cancellation for async functions, and side effects. A cache is appropriate only when the function’s inputs identify its result and stale results are acceptable under the chosen policy. Caching a function that depends on hidden state can return incorrect results.
Decorators have no inherent service cost, but their behavior can change application costs: retries can increase requests, caches can consume memory, and instrumentation can add processing or export traffic. Measure those effects in the application’s real workload rather than assuming every wrapper is free.
11. Or skip the browser setup
If the function you need to decorate ultimately captures web pages, you can use ScreenshotNeo, a website screenshot API and MCP server from Yorker Media, instead of managing browser setup yourself. A single GET request returns an image or PDF. See the ScreenshotNeo API documentation.
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshot tools. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000.
Sign up for ScreenshotNeo’s free plan.
12. Frequently asked questions
Can a decorator change a function’s return type?
Yes. A decorator can return any replacement callable or value, but changing the contract can surprise callers and type checkers. Document the new behavior and preserve the original result when the goal is only to add surrounding work.
Can a decorator be used on a class?
Yes. A class decorator receives a class object and returns the object that is bound to the class name. It may modify, register, or replace the class.
Does the decorator run every time I call the function?
The decorator expression is applied when Python executes the definition. A wrapper it returns usually runs on each later call. A registration decorator may do its work only during definition processing.
Can I decorate a lambda?
You can pass a lambda to a decorator as an ordinary expression, but the @ syntax decorates a function or class definition. A named function is usually clearer when a decorator is involved.
Do I need to write a decorator instead of using a built-in?
No. Prefer standard tools such as functools.wraps, functools.cache, classmethod, and staticmethod when they provide the behavior you need.
Primary references
- PEP 318 explains decorator syntax, application order, and decorator factories.
- Python functools documentation describes
wrapsand the metadata it preserves. - Python functools cache documentation covers the standard cache decorator.


