ScreenshotNeo

BlogGuides

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.

By the ScreenshotNeo team30 September 20269 min read

Python Decorators Explained: Examples and Use Cases

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.

A wrapper decorator receives a function and returns a callable that adds behavior around it.
A wrapper decorator receives a function and returns a callable that adds behavior around it.
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.

A decorator factory separates setup options from the function and its later call arguments.
A decorator factory separates setup options from the function and its later 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 @classmethod and @staticmethod transform 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.cache and functools.lru_cache provide 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

  1. 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?
  2. Separate setup from runtime. Put decorator options in a factory. Keep per-call inputs in the wrapper’s *args and **kwargs.
  3. Preserve the contract. Forward arguments, return the original result, and allow exceptions to propagate unless changing that behavior is intentional.
  4. Use @wraps. Apply it to the wrapper with the function being wrapped as its argument.
  5. Check side effects. A retry or repeat decorator can execute writes, payments, messages, or other effects more than once.
  6. Check composition. Write down stacked decorator expansion when order influences behavior.
  7. 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