Python decorators are callables that transform a function, method, or class when its definition is processed. The @decorator line is shorthand for calling a decorator and rebinding the name. A decorator can wrap calls, register a callable, attach metadata, or replace a class binding. This guide shows the exact evaluation order, how to preserve metadata with functools.wraps, how decorator factories separate configuration from runtime arguments, and when the pattern is useful.
What a Python decorator does
Consider this definition:
@announce
def greet(name):
return f"Hello, {name}!"
Python processes the function definition first, then applies announce to the resulting function and assigns the returned object back to greet. Mentally expand it as:
def greet(name):
return f"Hello, {name}!"
greet = announce(greet)
The decorator therefore runs when the definition is executed (normally while the module is imported). Any wrapper code inside the decorator’s returned function runs later, each time the decorated callable is called.
The @ line is syntax, not a special kind of function
The expression after @ must produce a callable transformation. It may be a simple function such as announce, an attribute, or a call that creates a decorator. The syntax keeps the transformation next to the declaration instead of hiding a reassignment elsewhere in the module.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →#1 Best Overall
Stacked decorators apply from the bottom upward
For:
@outer
a@inner
def work():
...
the equivalent assignment is:
work = outer(inner(work))
inner receives the original function first. Its result is passed to outer. At call time, the wrapper created by outer is therefore the outermost layer. Reordering two decorators can change logging, authorization, caching, exception handling, and even whether a call occurs at all.
Write a basic wrapper decorator
A wrapper decorator accepts a function, defines another function that performs the extra work, calls the original, and returns the wrapper:
from functools import wraps
def announce(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@announce
def greet(name):
return f"Hello, {name}!"
print(greet("Maya"))
Calling greet("Maya") prints the announcement and then returns Hello, Maya!. The *args and **kwargs pass positional and keyword arguments through without imposing a new signature on the wrapper.
Why functools.wraps matters
Without @wraps(func), introspection commonly sees the wrapper’s name and docstring rather than the original function’s. Python’s functools.wraps is intended for this wrapper pattern: it copies selected attributes, including the name, qualified name, module, annotations, and docstring, and updates the wrapper’s attribute dictionary. Frameworks, documentation generators, tracebacks, and debugging tools can consequently identify the decorated function correctly.
Use wraps whenever you return a wrapper. It does not make the wrapper transparent in every possible way: the wrapper still controls argument handling, return values, exceptions, and side effects. Those behaviors are your decorator’s contract.
Decorator factories: configure the decorator first
When a decorator needs options, add an outer function (a factory). The call after @ supplies configuration; the returned function receives the target function; the wrapper receives call arguments at runtime.
Rank #2
from functools import wraps
def repeat(times):
if times < 1:
raise ValueError("times must be at least 1")
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def notify(message):
print(message)
notify("Ready")
There are three distinct stages:
- Configuration:
repeat(3)runs while the definition is processed and returnsdecorator. - Decoration: Python calls
decorator(notify); it creates and returnswrapper. - Invocation: each later call to
notifyenterswrapper, which calls the original function three times.
Keep configuration values in the factory’s closure. Keep per-call values in *args and **kwargs. Confusing these layers is the most common source of “missing argument” errors in parameterized decorators.
Decorators do more than wrap calls
A wrapper is only one form of transformation. Python’s decorator syntax also supports decorators that return a different callable, register a function, or alter a class binding.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Built-in method transformations
@classmethod turns a function into a method receiving the class as its first argument. @staticmethod keeps a function on the class namespace without automatically supplying an instance or class argument. These are transformations applied to the definition, not logging wrappers that run extra code around every call.
Registration and attributes
A decorator can insert a function into a registry and return the same function, allowing a later dispatcher to find it by name. It can also attach an attribute used by another part of the program. A registration decorator may have no call-time behavior at all; its important work happens once, when the module defining the function is executed.
Class decorators
A class decorator receives a class object and returns either that class or a replacement. This can add methods, register the class, or change the binding that the rest of the module uses. The same “transform, then rebind” rule applies as with functions.
Caching
Caching is another practical use: a decorator can store results keyed by arguments and return a saved result on later calls. Decide explicitly how keys, mutable arguments, expiration, and invalidation should work; a cache changes the function’s observable behavior and memory use.
How to choose a decorator pattern
| Question | Wrapper | Registration or transformation |
|---|---|---|
| When does the main effect happen? | Usually on each call to the returned wrapper. | Often while the definition is processed, with little or no call-time logic. |
| What is returned? | A callable that normally delegates to the original. | The original object, a modified object, or a replacement callable/class. |
| Does it need configuration? | Use a factory such as @repeat(3) when options are required. |
The factory can register names, attach settings, or select a replacement. |
| Should metadata remain visible? | Use @wraps on the wrapper. |
Preserve or copy relevant attributes when returning a replacement. |
| What does stacking change? | Each layer receives the result from the layer below it. | Registration order and replacement order can alter the final binding. |
Choose a decorator when the same policy belongs around several callables and placing that policy beside each declaration improves readability. Avoid one when it hides substantial control flow, changes a function’s contract unexpectedly, or would be clearer as an explicit helper call.
Reliable decorator design
- Preserve the contract: pass through arguments and return values unless changing them is the purpose of the decorator.
- Preserve metadata: apply
@wraps(original)to wrapper functions. - Keep side effects deliberate: code outside the wrapper runs at definition time; code inside runs at call time.
- Handle exceptions intentionally: re-raise by default if callers should observe the original failure; only convert or suppress exceptions as part of a documented policy.
- Make ordering obvious: document important stacking combinations and test the exact order used in production.
- Keep configuration immutable where practical: a factory’s closed-over settings are shared by every call to that decorated function.
Common errors and fixes
“My decorated function returns None.”
The wrapper probably calls the original but omits return. Return the result when the original function’s value is part of its contract.
“The function name or docstring says wrapper.”
Apply @wraps(func) directly above the inner wrapper definition. The decorator must import it from functools.
“A configured decorator says an argument is missing.”
Check the three layers. @factory(option) calls the factory; the factory must return a decorator; that decorator must accept the function and return a wrapper. Runtime arguments belong in the wrapper, not in the factory.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →“Two decorators run in the wrong order.”
Rewrite the stack as nested calls. The bottom line is applied first: @outer over @inner means outer(inner(function)). Swap the lines or change the explicit composition to get the desired order.
“The decorator runs too early.”
Move per-call work inside the wrapper. Code in the decorator factory or decorator body executes while the module definition is processed, not when the function is eventually called.
“A method receives unexpected arguments.”
Ensure the wrapper accepts and forwards both positional and keyword arguments. If the decorator changes binding behavior, verify whether the target is an instance method, class method, or static method and apply the built-in transformation in the intended order.
A practical decorator around a ScreenshotNeo request
The same pattern can add consistent logging or timing around an HTTP call without changing the call site. ScreenshotNeo is a website screenshot API and MCP server; one request can return a PNG, JPEG, WebP, or PDF. The decorator below logs the URL while leaving the request function’s result untouched.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsfrom functools import wraps
import requests
def log_capture(func):
@wraps(func)
def wrapper(url, *args, **kwargs):
print(f"Capturing {url}")
return func(url, *args, **kwargs)
return wrapper
@log_capture
def capture(url, access_key):
response = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": access_key, "url": url},
timeout=90,
)
response.raise_for_status()
return response.content
image_bytes = capture("https://stripe.com", "YOUR_API_KEY")
open("shot.webp", "wb").write(image_bytes)
For the service’s complete parameter list and authentication details, see the ScreenshotNeo documentation.
Or skip the browser setup
If your goal is simply to obtain a clean page image, ScreenshotNeo accepts one GET request. These examples use the supplied endpoint and save the response locally.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Before capture, ScreenshotNeo accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. Every response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 screenshots per month with no card. Paid plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free, and every feature is included on every plan. Create a free ScreenshotNeo account to start.
Performance, reliability, and maintenance
A wrapper adds at least one extra Python function call and whatever work you place around the original. For inexpensive, frequently called functions, keep logging, serialization, and cache-key construction small. For I/O-bound work, the external operation usually dominates, but the decorator can still affect latency if it performs repeated setup.
Best Value
Reliability depends on preserving the original control flow. Forward arguments exactly, return the original result, and avoid swallowing exceptions unless callers have an alternative recovery path. If a decorator performs registration or other import-time work, importing the module can now fail for reasons unrelated to a later function call; test imports as well as calls.
Maintenance is easier when each decorator has one policy, a narrow name, and tests for both the undecorated behavior and the decorated behavior. Test stacked forms explicitly because changing one line in a decorator stack changes the callable each layer receives.
Frequently Asked Questions
Can I use a decorator without changing the original function’s source code?
Yes. Apply it where the function is defined or rebind the function afterward with an equivalent assignment such as name = decorator(name).
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWhat should a decorator return if it only records a function?
Usually return the original function after registering it, so callers retain the same callable and metadata. Return a replacement only when changing the binding is intentional.
How can I inspect what a decorated function represents?
Use functools.wraps for wrapper decorators so standard introspection sees the wrapped function’s identifying metadata rather than only the wrapper’s defaults.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

