Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A Python decorator is a callable that transforms a function, method, or class when its definition is processed. The @decorator line is convenient syntax: Python creates the definition, passes it to the decorator, and binds the returned object to the original name. A decorator can wrap calls, register a function, attach attributes, or replace a class; it does not have to be a wrapper that runs on every call.

This guide explains the transformation model, metadata-preserving wrappers, configurable decorator factories, stacking order, practical use cases, testing, and common failure modes.

What @decorator means

Consider this definition:

@announce
def greet(name):
    return f"Hello, {name}!"

Python treats it essentially as:

def greet(name):
    return f"Hello, {name}!"
greet = announce(greet)

The decorator expression is evaluated and called with the newly created function. Whatever it returns becomes the value bound to greet. This rebinding happens while the module (or containing class body) is being executed, before a caller invokes greet.

The syntax keeps the transformation next to the declaration. It also works with any callable transformation, including a class, an object implementing __call__, or a factory that returns a decorator.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How a wrapper decorator works

A wrapper-based decorator has three jobs: accept the original function, define a replacement that adds behavior, and return that replacement.

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("Mina"))

Calling greet("Mina") actually calls wrapper. The wrapper logs the call, forwards positional and keyword arguments, and returns the original result. *args and **kwargs make the decorator usable with many function signatures; a specialized wrapper can instead declare explicit parameters when that improves validation or readability.

Why functools.wraps matters

Without @wraps(func), introspection sees the wrapper’s name, docstring, annotations, and other metadata rather than the decorated function’s. functools.wraps is intended for exactly this pattern. It copies selected attributes such as __name__, __qualname__, __module__, __annotations__, and __doc__, and updates the wrapper’s attribute dictionary. It also sets __wrapped__, allowing tools and other decorators to reach the original callable.

from inspect import signature

print(greet.__name__)       # greet
print(greet.__doc__)        # the original docstring, if present
print(signature(greet))     # commonly follows the wrapped function

Use wraps whenever you return a wrapper unless you have a specific reason not to. Preserving metadata helps debuggers, documentation generators, tracing tools, and tests.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Definition time versus call time

The decorator itself runs when Python executes the decorated definition. Code inside the returned wrapper runs each time the function is called. Keeping those moments separate prevents subtle bugs.

  • Definition time: configuration is read, registration can occur, and the original function can be replaced.
  • Call time: a wrapper can inspect arguments, enforce a policy, measure duration, catch or translate exceptions, and alter the return value.
  • Neither is required: a decorator may simply attach an attribute or return a different object without wrapping calls.

For example, a registration decorator can collect functions for later dispatch:

COMMANDS = {}

def command(name):
    def register(func):
        COMMANDS[name] = func
        return func
    return register

@command("hello")
def hello_command():
    return "Hello"

# The function remains callable, and registration happened at definition time.
print(COMMANDS["hello"]())

Parameterized decorators and decorator factories

If the decorator needs options, the expression after @ must first call a factory. That creates three layers:

  1. The factory receives configuration.
  2. The returned decorator receives the function.
  3. The returned wrapper receives runtime call arguments.
from functools import wraps

def announce_with(prefix):                 # 1. configuration
    def decorator(func):                    # 2. function
        @wraps(func)
        def wrapper(*args, **kwargs):       # 3. call arguments
            print(f"{prefix}: {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@announce_with("AUDIT")
def add(a, b):
    return a + b

print(add(2, 3))

@announce_with("AUDIT") is equivalent to add = announce_with("AUDIT")(add). A frequent mistake is omitting one layer and accidentally passing a string where Python expects a function, or returning the decorator instead of the wrapper.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Stacking decorators: which one runs first?

With stacked decorators, the decorator nearest the function is applied first:

@outer
a@inner
def task():
    pass

Ignoring the typo-like spacing above, the intended form is:

@outer
@inner
def task():
    pass

Its expansion is:

def task():
    pass
task = outer(inner(task))

inner receives the original function. outer receives whatever inner returned. At call time, the outer wrapper normally executes first and can decide whether to call the inner one.

Order changes behavior. A cache placed outside an authorization check can return a cached result before authorization runs; placing authorization outside the cache checks every request. Write a small expansion on paper when order is not obvious, and test both success and failure paths.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Useful decorator patterns

Timing and logging

from functools import wraps
from time import perf_counter

def timed(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            elapsed = perf_counter() - start
            print(f"{func.__name__}: {elapsed:.6f}s")
    return wrapper

The finally block records failures as well as successful calls. Avoid logging sensitive arguments by default.

Validation and authorization

A policy decorator can reject a call before invoking the function, or check a returned value afterward. Keep the contract explicit: document the exception raised, the required argument, and whether the wrapped function is called at all on failure.

Caching

Caching is a common use case when a function is deterministic for its inputs. Python’s standard library provides caching decorators; if you write your own, define the cache key rules, memory limits, invalidation behavior, and thread or process scope. Never cache values that depend on hidden mutable state unless that dependency is part of the invalidation design.

Method transformation

classmethod and staticmethod are built-in examples of decorators that change method binding. A class method receives the class as its first argument; a static method receives no implicit instance or class argument.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Registration and attributes

A decorator can add a marker attribute, register a callback, or return a replacement class. These transformations happen at definition time and may be preferable to a runtime wrapper when no per-call work is needed.

Writing robust decorators

  • Use @wraps for wrappers.
  • Forward arguments and return values unchanged unless changing them is the documented purpose.
  • Preserve exceptions unless the decorator intentionally translates or handles them.
  • Keep side effects visible; registration and configuration should not be surprising.
  • Make decorator order part of the design and test it explicitly.
  • Prefer a normal helper function when behavior is used once or when the wrapper would obscure control flow.
  • For async functions, write an async def wrapper and await the original; a synchronous wrapper that merely returns a coroutine may put timing, exception handling, or context management in the wrong place.

Testing and troubleshooting

The function name or docstring changed

Add from functools import wraps and place @wraps(func) directly above the inner wrapper. Confirm that the decorator is wrapping the intended function rather than a second wrapper.

“Missing required positional argument” or unexpected keyword errors

The wrapper’s signature may not forward arguments correctly. Use *args, **kwargs and call func(*args, **kwargs), or deliberately mirror the original signature and test positional and keyword calls.

The configured decorator receives a function too early

Check the layers. @limit(3) requires limit to return a decorator; @limit requires limit itself to accept the function. Do not mix these forms.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Behavior runs in the wrong order

Expand the stack into assignments such as f = outer(inner(f)). Swap the lines if the policy, cache, transaction, or logging boundary is wrong, then add a regression test documenting the intended order.

Methods behave differently from functions

Remember that instance methods receive self after descriptor binding. A decorator that replaces a method with a plain object can prevent normal binding. Test access through an instance and through the class.

Debugging the original callable

inspect.unwrap(decorated_function) can follow the __wrapped__ chain created by wraps. This is useful for tests and introspection, but it does not undo side effects already performed by a decorator.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When a decorator is the right tool

Choose one when the same cross-cutting behavior belongs around multiple callables and placing that behavior beside each declaration improves comprehension. Logging, authorization, retries, caching, registration, and instrumentation are typical fits. Avoid decorators when the behavior needs many unrelated configuration values, has complicated branching that hides the business operation, or would be clearer as an explicit function call or context manager.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A decorator is a small language feature with a broad contract: it transforms a definition, and the transformed object may wrap, register, annotate, or replace it. Knowing exactly which layer receives configuration, the function, and call arguments is the key to writing one correctly.

Or skip the browser setup: ScreenshotNeo for automated page captures

Decorators are useful for wrapping an HTTP client call that captures a page, but you do not need to configure a headless browser yourself. ScreenshotNeo provides a single screenshot API request:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for parameters and response details. Cookie and consent banners, newsletter popups, and chat widgets are removed before capture. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

Frequently Asked Questions

Can a decorator be applied to a class?

Yes. A class decorator receives the class object created by the class statement and returns that class or a replacement. It can register the class, attach attributes, or alter its binding.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What does a decorator return if it does not wrap a function?

It returns the object that should replace the original binding, which may be the unchanged function, a registered function, a descriptor, a class, or another callable.

How can I inspect several layers of decorators?

Use metadata-preserving wrappers and inspect the callable’s __wrapped__ chain or call inspect.unwrap. Without functools.wraps, that chain may not exist.

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.