At its core, a decorator is a higher-order function—a function that takes another function as an argument and returns a new function, usually with enhanced or altered behavior. The decorator syntax @decorator is merely syntactic sugar that applies this function transformation in a declarative and readable way, directly above the function definition. This pattern is a powerful application of Python’s first-class functions and closures, allowing you to modify the behavior of functions or methods without permanently modifying their source code.

The Anatomy of a Basic Decorator

A simple decorator is constructed from two nested functions. The outer function, the decorator itself, accepts the function to be decorated (often called func). The inner function, typically named wrapper, is the new function that will replace the original. Its parameters (*args and **kwargs) allow it to handle any set of arguments the original function might receive.

def simple_decorator(func):
    """A decorator that prints a message before and after the function call."""
    def wrapper(*args, **kwargs):
        print(f"About to call {func.__name__}")
        result = func(*args, **kwargs)  # Call the original function
        print(f"Finished calling {func.__name__}")
        return result  # Return the original function's result
    return wrapper

@simple_decorator
def greet(name):
    print(f"Hello, {name}!")

greet("Alice")

Output:

About to call greet
Hello, Alice!
Finished calling greet

The @simple_decorator syntax is equivalent to writing greet = simple_decorator(greet). When Python encounters the @ line during compilation, it immediately executes the decorator function, passing in the decorated function (greet). The decorator returns the wrapper function, which becomes the new value bound to the name greet. When you later call greet("Alice"), you are actually calling the wrapper function.

Preserving Function Identity and Metadata

A critical pitfall of the basic decorator pattern is that the original function’s metadata (like its name, docstring, and module) is obscured by the wrapper. The greet function now reports itself as wrapper, which breaks introspection and tools like help().

print(greet.__name__)  # Output: 'wrapper'
help(greet)            # Shows help for wrapper, not greet

To solve this, the functools.wraps decorator should always be used inside your decorator. It copies the metadata from the original function to the wrapper function, preserving the decorated function’s identity.

import functools

def proper_decorator(func):
    @functools.wraps(func)  # This preserves func's metadata
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@proper_decorator
def greet(name):
    """A friendly greeting."""
    print(f"Hello, {name}!")

print(greet.__name__)  # Output: 'greet'
print(greet.__doc__)   # Output: 'A friendly greeting.'

Decorators and Functions That Take Arguments

The wrapper function uses *args and **kwargs to be a generic pass-through for any function signature. This is what makes a decorator reusable across different functions. The decorator itself should almost never need to know the specifics of the function it’s wrapping; its job is to add behavior around the function call, not to interfere with the call itself. The line result = func(*args, **kwargs) is where the original function is executed with all the arguments it was passed. The result is captured and returned, ensuring the decorated function’s return value is not lost.

Best Practices and Common Pitfalls

  1. Always use @functools.wraps: This is non-negotiable for maintaining debuggability and correct behavior.
  2. Ensure the wrapper returns the value: Forgetting to return result from the wrapper is a common mistake. If the original function returns a value, the decorated function must return it as well, otherwise it will implicitly return None.
  3. Be cautious with stateful decorators: The basic pattern shown is stateless. If your decorator needs to maintain state across calls (e.g., a counter), you must structure it differently, often using a class or nonlocal variables, which is a more advanced topic.
  4. Decorators execute at import time: The decorator function runs immediately when the module is imported, not when the decorated function is called. This is the point where func is passed in and wrapper is created and returned. The actual code inside wrapper runs on each subsequent call.