Python Decorators: Execution of the “Before” Code

Every Python decorator follows the same structural contract: wrap a function, optionally run logic before it, call the original, and optionally run logic after it. The code that sits above the func(*args, **kwargs) call inside the wrapper is the before code, and it is the single point of control that determines whether the original function runs at all, what state exists when it starts, and what information gets captured on the way in. Understanding how and when this before code executes is essential for writing decorators that do more than print a message.

When you apply a decorator with the @ syntax, Python replaces the original function with whatever the decorator returns. In nearly every case, that returned object is a wrapper function containing three regions of code: the before region (everything above func()), the call itself, and the after region (everything below it). This article focuses entirely on that first region and the patterns it enables, from simple logging to full conditional gating that can prevent the original function from executing altogether.

The Anatomy of Before Code in Decorators

A decorator is a callable that accepts a function and returns a new function. The returned wrapper function is what callers invoke instead of the original. Inside that wrapper, any code placed before the line that calls the original function is the before code. Here is the simplest possible illustration:

from functools import wraps


def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # === BEFORE CODE ===
        print(f"About to call {func.__name__}")

        # === THE CALL ===
        result = func(*args, **kwargs)

        # === AFTER CODE ===
        print(f"Finished calling {func.__name__}")
        return result
    return wrapper


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


greet("Kandi")
# Output:
# About to call greet
# Hello, Kandi
# Finished calling greet

The print statement that reads "About to call greet" is the before code. It runs every time greet() is called, before the body of the original greet function begins execution. The wrapper captures all positional and keyword arguments through *args and **kwargs, which means the before code has full access to every argument that was passed to the decorated function.

This access is what makes before code so powerful. The wrapper can inspect arguments, validate them, log them, modify them, or use them to make decisions about whether to proceed with the call at all. This works because the wrapper function forms a closure over the original function, retaining a reference to func even after the decorator has returned.

Note

The before code does not have access to the return value of the original function. That value only exists after func(*args, **kwargs) completes. If you need to inspect or modify the return value, that logic belongs in the after code section.

Accessing Arguments in the Before Code

Because the wrapper receives the same *args and **kwargs that were intended for the original function, the before code can unpack and inspect them. This is the foundation for input validation decorators, argument logging, and type-checking wrappers:

from functools import wraps


def log_arguments(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # BEFORE CODE: inspect and log every argument
        arg_strs = [repr(a) for a in args]
        kwarg_strs = [f"{k}={v!r}" for k, v in kwargs.items()]
        all_args = ", ".join(arg_strs + kwarg_strs)
        print(f"[LOG] {func.__name__}({all_args})")

        return func(*args, **kwargs)
    return wrapper


@log_arguments
def transfer_funds(sender, receiver, amount, currency="USD"):
    print(f"Transferring {amount} {currency} from {sender} to {receiver}")


transfer_funds("Alice", "Bob", 500, currency="EUR")
# Output:
# [LOG] transfer_funds('Alice', 'Bob', 500, currency='EUR')
# Transferring 500 EUR from Alice to Bob

Every time transfer_funds is called, the before code reconstructs a human-readable representation of the arguments and prints it. The original function has no awareness that this logging occurred. This separation is exactly what makes decorators effective for cross-cutting concerns that need to apply uniformly across many functions.

Definition Time vs. Call Time

One of the areas that causes confusion is the distinction between when the decorator runs and when the before code runs. These are two different moments in the program's lifecycle, and conflating them leads to bugs that are difficult to trace.

When Python encounters the @my_decorator syntax during module loading, it immediately calls my_decorator(func). This happens at definition time, the moment the def statement is processed. The decorator function body executes, the wrapper is created, and the original function name is rebound to the wrapper. Any code inside the decorator function but outside the wrapper runs once, at definition time.

The before code, by contrast, lives inside the wrapper. It runs at call time, every time someone invokes the decorated function. Here is an example that makes the distinction visible:

from functools import wraps


def timing_decorator(func):
    # This print runs ONCE at definition time
    print(f"[DEFINITION TIME] Decorating {func.__name__}")

    @wraps(func)
    def wrapper(*args, **kwargs):
        # This print runs at CALL TIME, every invocation
        print(f"[CALL TIME] Before executing {func.__name__}")
        result = func(*args, **kwargs)
        return result
    return wrapper


@timing_decorator
def compute(x, y):
    return x + y


# At this point, "Decorating compute" has already printed.
# No calls have been made yet.

print("--- First call ---")
compute(3, 4)

print("--- Second call ---")
compute(10, 20)

Running this produces:

[DEFINITION TIME] Decorating compute
--- First call ---
[CALL TIME] Before executing compute
--- Second call ---
[CALL TIME] Before executing compute

The definition-time message appears once, before any call happens. The call-time before code appears twice, once per invocation. This distinction matters because before code that is supposed to run per-call will silently fail if you accidentally place it in the decorator body instead of inside the wrapper.

Pro Tip

If you need to perform a one-time setup when the decorator is applied (such as registering the function in a lookup table), place that logic in the decorator body. If you need per-invocation behavior (such as logging or validation), it belongs inside the wrapper as before code.

Why functools.wraps Matters for Before Code

When a decorator replaces the original function with a wrapper, the wrapper has its own __name__, __doc__, and __module__ attributes. Without @wraps(func), any before code that references func.__name__ works correctly, but external tools inspecting the decorated function see the wrapper's metadata instead. The @wraps decorator from functools copies the original function's metadata onto the wrapper, preserving introspection for debuggers, documentation generators, and frameworks:

from functools import wraps


def before_logger(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling: {func.__name__}")
        return func(*args, **kwargs)
    return wrapper


@before_logger
def process_data(payload):
    """Process the incoming data payload."""
    return payload


# Metadata is preserved thanks to @wraps
print(process_data.__name__)   # process_data
print(process_data.__doc__)    # Process the incoming data payload.

Without @wraps(func), the __name__ would be "wrapper" and the docstring would be None. This is not cosmetic. Frameworks like Flask and Django use function names for route registration and URL resolution. Broken metadata can cause silent routing failures.

The Mental Model: Airport Security

If you find the distinction between definition time and call time difficult to hold in your head, try this analogy. Think of a decorator as installing a security checkpoint at an airport gate. Installing the checkpoint (building the queue ropes, positioning the scanner, assigning staff) happens once, when the gate opens for the day. That is definition time. But the actual screening of each passenger (checking their boarding pass, scanning their bag, waving them through or pulling them aside) happens every time a passenger approaches the gate. That is call time.

The before code is the screening procedure itself: it sees every passenger (every set of arguments), it can refuse entry (raise an exception or return early), it can stamp the boarding pass (modify arguments), and it can log who went through (record metadata). The original function is the airplane. The before code never changes the airplane, but it decides who gets on it and what state they are in when they board.

This analogy also clarifies why @wraps(func) matters. Without it, every gate displays the name "Security Checkpoint" instead of the actual flight number. Passengers (developers, frameworks, debuggers) looking for their flight cannot find it because the original identity has been overwritten. The @wraps decorator restores the flight number to the gate sign.

Check Your Understanding

When Does This Code Print?

Given the following decorator, at what point does the message "Registering handler" appear?

from functools import wraps

def register(func):
    print("Registering handler")

    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Executing handler")
        return func(*args, **kwargs)
    return wrapper


@register
def on_click(event):
    print(f"Clicked: {event}")
A When on_click(event) is called for the first time
B Immediately when Python processes the @register line, before any call
C Every time on_click(event) is called, before "Executing handler"

Practical Before-Code Patterns

The before code region inside a decorator wrapper is where all pre-execution concerns live. The following patterns represent the categories that appear in production codebases.

Pattern 1: Input Validation

A validation decorator inspects arguments before allowing the original function to proceed. If validation fails, the before code raises an exception or returns an error value without ever calling func():

from functools import wraps


def validate_positive(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # BEFORE CODE: check every positional argument
        for i, arg in enumerate(args):
            if isinstance(arg, (int, float)) and arg < 0:
                raise ValueError(
                    f"Argument at position {i} must be positive, got {arg}"
                )

        # Check keyword arguments as well
        for key, value in kwargs.items():
            if isinstance(value, (int, float)) and value < 0:
                raise ValueError(
                    f"Keyword argument '{key}' must be positive, got {value}"
                )

        return func(*args, **kwargs)
    return wrapper


@validate_positive
def calculate_area(length, width):
    return length * width


print(calculate_area(5, 10))       # 50
calculate_area(-3, 10)             # Raises ValueError

The critical detail here is that func(*args, **kwargs) is never reached when validation fails. The before code raises ValueError, which unwinds the call stack before the original function body has a chance to execute. This is the gating pattern: the before code acts as a guard that either permits or denies entry to the original function.

Pattern 2: Authentication and Authorization

Web frameworks use this pattern extensively. The before code checks whether a user has the correct credentials or permissions before allowing a request handler to execute:

from functools import wraps


def require_role(role):
    """Parameterized decorator that checks user role before execution."""
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            # BEFORE CODE: verify user role
            if not hasattr(user, "role"):
                raise AttributeError("User object has no 'role' attribute")

            if user.role != role:
                print(f"Access denied: {user.name} has role '{user.role}', "
                      f"requires '{role}'")
                return None

            print(f"Access granted: {user.name} ({user.role})")
            return func(user, *args, **kwargs)
        return wrapper
    return decorator


class User:
    def __init__(self, name, role):
        self.name = name
        self.role = role


@require_role("admin")
def delete_database(user, db_name):
    print(f"Deleting database: {db_name}")
    return True


admin = User("Kandi", "admin")
viewer = User("Guest", "viewer")

delete_database(admin, "test_db")
# Access granted: Kandi (admin)
# Deleting database: test_db

delete_database(viewer, "production_db")
# Access denied: Guest has role 'viewer', requires 'admin'

This is a three-layer decorator, the outermost function (require_role) accepts the configuration parameter, the middle function (decorator) accepts the function, and the innermost function (wrapper) contains the before code. The before code inspects the first argument (the user), checks its role, and either proceeds with the call or returns None to deny access. The original function delete_database never sees the guest user's invocation.

Pattern 3: Timing and Performance Measurement

A timing decorator records a start timestamp in the before code and compares it with an end timestamp in the after code. The before code's role is to capture the moment just prior to execution:

import time
from functools import wraps


def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # BEFORE CODE: record start time
        start = time.perf_counter()

        result = func(*args, **kwargs)

        # AFTER CODE: calculate elapsed time
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} completed in {elapsed:.4f} seconds")
        return result
    return wrapper


@timer
def slow_computation(n):
    total = 0
    for i in range(n):
        total += i ** 2
    return total


slow_computation(1_000_000)
# slow_computation completed in 0.0823 seconds

The before code here is a single line: start = time.perf_counter(). Its simplicity is deceptive. Without that one line, the after code would have no reference point. This demonstrates that before code does not need to be complex to be essential. It establishes the state that the rest of the wrapper depends on.

Pattern 4: Conditional Gating with Early Return

Sometimes the before code should prevent execution entirely based on external state, not just argument values. A feature flag decorator, for example, checks a configuration before deciding whether to run the function or return a fallback:

from functools import wraps

# Simulated feature flag configuration
FEATURE_FLAGS = {
    "new_search_algorithm": True,
    "experimental_cache": False,
}


def feature_gate(flag_name, fallback=None):
    """Only execute the function if the feature flag is enabled."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # BEFORE CODE: check feature flag
            if not FEATURE_FLAGS.get(flag_name, False):
                print(f"[GATE] Feature '{flag_name}' is disabled, "
                      f"returning fallback")
                return fallback

            print(f"[GATE] Feature '{flag_name}' is enabled, proceeding")
            return func(*args, **kwargs)
        return wrapper
    return decorator


@feature_gate("new_search_algorithm")
def search(query):
    print(f"Executing new search for: {query}")
    return [f"result for {query}"]


@feature_gate("experimental_cache", fallback=[])
def get_cached_results(key):
    print(f"Fetching from experimental cache: {key}")
    return ["cached_item_1", "cached_item_2"]


print(search("python decorators"))
# [GATE] Feature 'new_search_algorithm' is enabled, proceeding
# Executing new search for: python decorators
# ['result for python decorators']

print(get_cached_results("homepage"))
# [GATE] Feature 'experimental_cache' is disabled, returning fallback
# []

The before code in this decorator performs a dictionary lookup against the feature flag configuration. If the flag is off, the wrapper returns the fallback value immediately. The original function get_cached_results never executes. This pattern is used heavily in large systems where new features need to be deployed behind flags that can be toggled without redeployment.

Pattern 5: Resource Acquisition

Before code is the natural place to acquire resources that the function needs, such as database connections, file handles, or thread locks:

import threading
from functools import wraps

_lock = threading.Lock()


def thread_safe(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # BEFORE CODE: acquire the lock via context manager
        # The with statement calls _lock.acquire() on entry
        # and guarantees _lock.release() on exit, even if
        # the function raises an exception.
        with _lock:
            return func(*args, **kwargs)
    return wrapper


shared_counter = 0


@thread_safe
def increment():
    global shared_counter
    current = shared_counter
    shared_counter = current + 1
    return shared_counter


print(increment())  # 1
print(increment())  # 2

The before code here is the with _lock: statement. The with statement calls _lock.acquire() on entry and guarantees _lock.release() on exit, even if the function raises an exception. This is the recommended approach for lock management in Python because it eliminates the risk of forgetting to release the lock, a common source of deadlocks. The before code acquires the resource, and the context manager handles the cleanup automatically when the block exits.

Pattern Before Code Responsibility Can Prevent func() Call?
Argument Logging Capture and print/record arguments No
Input Validation Check argument constraints Yes (raises exception)
Authentication Verify user identity or role Yes (returns early)
Timing Record start timestamp No
Feature Gating Check external flag/config Yes (returns fallback)
Resource Acquisition Acquire lock, connection, handle Depends on acquisition outcome
Spot the Bug

This Decorator Has a Defect

The following decorator is supposed to log function calls and then execute the original function normally. It was written by a developer who understands the basic decorator structure but made a mistake that causes a subtle runtime failure. Can you identify the bug?

 1from functools import wraps
 2
 3
 4def log_call(func):
 5    @wraps(func)
 6    def wrapper(*args, **kwargs):
 7        print(f"Calling {func.__name__}")
 8        func(*args, **kwargs)
 9    return wrapper
10
11
12@log_call
13def add(a, b):
14    return a + b
15
16
17result = add(3, 4)
18print(result)
A Line 7: func.__name__ fails because @wraps overwrites the name before the wrapper runs
B Line 8: The wrapper calls func() but does not return its result, so add(3, 4) always returns None
C Line 6: The wrapper accepts *args, **kwargs but add() only takes two positional arguments, so the signature is mismatched

Stacked Decorators and Before-Code Order

When multiple decorators are stacked on a single function, their before code executes in a specific and predictable order. Understanding this order of execution in chained decorators is necessary for writing decorator chains that behave correctly.

Decorators are applied bottom-to-top at definition time. The decorator closest to the def keyword wraps the function first. Each subsequent decorator wraps the result of the previous one. However, when the decorated function is called, execution flows top-to-bottom through the before code of each wrapper, then reaches the original function, and then flows bottom-to-top through the after code.

from functools import wraps


def outer_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("[OUTER] Before code executing")
        result = func(*args, **kwargs)
        print("[OUTER] After code executing")
        return result
    return wrapper


def inner_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("[INNER] Before code executing")
        result = func(*args, **kwargs)
        print("[INNER] After code executing")
        return result
    return wrapper


@outer_decorator
@inner_decorator
def process():
    print("[ORIGINAL] Function body executing")


process()

Output:

[OUTER] Before code executing
[INNER] Before code executing
[ORIGINAL] Function body executing
[INNER] After code executing
[OUTER] After code executing

The stacking @outer_decorator followed by @inner_decorator is equivalent to writing process = outer_decorator(inner_decorator(process)). At call time, the outer wrapper's before code runs first because it is the outermost layer. It then calls what it thinks is the original function, which is the inner wrapper. The inner wrapper's before code runs next. Only then does the original process function execute. On the way back out, the after code unwinds in reverse: inner first, then outer.

This means that if you want validation to happen before logging, you should place the validation decorator below the logging decorator in the stack so that validation's before code runs after logging's before code. Or, depending on your intent, place the validation decorator above logging so that invalid calls are rejected before they are logged. The order you choose determines the behavior.

A Realistic Stacked Example

Consider a function that needs both timing and input validation. The timer should measure only the time spent inside the validated function, not the validation overhead itself. This requires placing the timer inside (below) the validator in the stack:

import time
from functools import wraps


def validate_nonzero(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # BEFORE CODE: reject zero denominators
        for arg in args:
            if arg == 0:
                raise ZeroDivisionError(
                    "Arguments must be non-zero"
                )
        return func(*args, **kwargs)
    return wrapper


def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # BEFORE CODE: start clock
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__}: {elapsed:.6f}s")
        return result
    return wrapper


@validate_nonzero   # Outer: runs its before code first
@timer              # Inner: runs its before code second
def divide(a, b):
    time.sleep(0.01)  # Simulate work
    return a / b


print(divide(10, 2))
# divide: 0.010234s
# 5.0

The validation before code runs first. If it passes, the timer's before code runs next and captures the start time. The elapsed time reported by the timer excludes the validation overhead because the timer wrapper only sees the inner function call. If validation fails, the timer's before code never runs at all because the validation wrapper raises an exception before reaching the func() call that would invoke the timer's wrapper.

Warning

Reversing the stack order would cause the timer to measure validation time as well, which would inflate the reported duration. When stacking decorators, always reason about which before code should execute first and which measurements or checks should be isolated from each other.

Predict the Output

What Prints When save("data.csv") Runs?

Read the following stacked decorator setup carefully. Each decorator has distinct before and after code. Predict what the full console output will be when save("data.csv") is called.

from functools import wraps


def authorize(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("[AUTH] Checking permissions")
        result = func(*args, **kwargs)
        print("[AUTH] Permissions verified")
        return result
    return wrapper


def log_io(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("[LOG] Write operation started")
        result = func(*args, **kwargs)
        print("[LOG] Write operation finished")
        return result
    return wrapper


@authorize
@log_io
def save(filename):
    print(f"[SAVE] Writing {filename}")


save("data.csv")

Which sequence matches the actual output?

A [LOG] Write operation started
[AUTH] Checking permissions
[SAVE] Writing data.csv
[AUTH] Permissions verified
[LOG] Write operation finished
B [AUTH] Checking permissions
[LOG] Write operation started
[SAVE] Writing data.csv
[LOG] Write operation finished
[AUTH] Permissions verified
C [AUTH] Checking permissions
[SAVE] Writing data.csv
[LOG] Write operation started
[LOG] Write operation finished
[AUTH] Permissions verified

Key Takeaways

  1. Before code is everything above func(*args, **kwargs) inside the wrapper. It runs every time the decorated function is called, has full access to the arguments, and can prevent the original function from executing.
  2. Definition time and call time are separate events. The decorator body runs once when the function is defined. The before code inside the wrapper runs every time the function is called. Placing per-call logic in the wrong location causes it to execute only once.
  3. Before code enables gating patterns. Validation, authentication, and feature flags all use before code to decide whether the original function should execute. Raising an exception or returning early from the wrapper skips the func() call entirely.
  4. Stacked decorators execute before code top-to-bottom at call time. The outermost decorator's before code runs first. If it calls its func(), the next decorator's before code runs, and so on until the original function is reached.
  5. Always use @functools.wraps. It preserves the original function's __name__, __doc__, and other metadata, which prevents subtle bugs in frameworks and debugging tools that rely on function identity.

The before code in a Python decorator is not an afterthought. It is the control plane that determines what happens before the original function begins. Whether you are recording timestamps, checking permissions, validating inputs, or acquiring resources, the logic you place above func(*args, **kwargs) defines the preconditions under which the original function is allowed to run. Mastering this region of the wrapper gives you fine-grained control over function execution without modifying a single line of the original function's code.

How to Write Before Code in a Python Decorator

  1. 01

    Define the decorator and wrapper

    Create a decorator function that accepts a function as its argument. Inside it, define a wrapper function using *args and **kwargs to accept any arguments. Apply @functools.wraps(func) to the wrapper to preserve the original function's metadata.

  2. 02

    Write the before code above func()

    Place any pre-execution logic above the func(*args, **kwargs) call inside the wrapper. This code runs every time the decorated function is invoked and has full access to the incoming arguments.

  3. 03

    Decide whether to gate or pass through

    If the before code should conditionally prevent execution, add a check that raises an exception or returns early before the func() call. If it should always pass through, let execution fall through to func(*args, **kwargs) after the before code completes.

  4. 04

    Return the result of func()

    Always capture the return value of func(*args, **kwargs) and return it from the wrapper. Omitting the return statement is a common bug that causes the decorated function to silently return None.

  5. 05

    Test definition-time vs call-time behavior

    Verify that any code in the decorator body outside the wrapper runs once at definition time, and that the before code inside the wrapper runs on every call. Place a print statement in each location to confirm the timing.

Frequently Asked Questions

What does "before code" mean in a Python decorator?
Before code refers to any logic placed inside the wrapper function that runs prior to calling the original decorated function. This code executes every time the decorated function is invoked, before the original function body begins.
When does a decorator's before code run: at definition time or call time?
The decorator itself runs at definition time when Python encounters the @decorator syntax. However, the before code inside the wrapper function runs at call time, each time the decorated function is invoked.
Can the before code in a decorator prevent the original function from executing?
Yes. Because the before code runs prior to the func() call inside the wrapper, it can include conditional checks, raise exceptions, or return early. If the wrapper returns before calling func(), the original function never executes.
What are common uses for before code in Python decorators?
Common uses include input validation, authentication and authorization checks, logging function entry, recording start timestamps for timing, acquiring locks or resources, and printing debug information about function arguments.
Does the order of stacked decorators affect which before code runs first?
Yes. Stacked decorators are applied bottom-to-top at definition time but their before code executes top-to-bottom at call time. The outermost decorator's before code runs first, followed by each inner decorator's before code, before the original function executes.