Skip to main content

Decorators

This guide covers advanced decorator concepts and patterns. For fundamental decorator concepts, see the guide on decorators.

Execution timing: Definition time vs call time​

This is crucial: decorators run at definition time, not call time. The decorator executes when Python defines the function, not when you call it.

Definition time execution​

def decorator(func):
print(f"Decorator running for {func.__name__}")
def wrapper():
print(f"Wrapper running for {func.__name__}")
return func()
return wrapper

@decorator
def greet():
print("Hello!")

print("Function defined")
greet()
# Output:
# Decorator running for greet
# Function defined
# Wrapper running for greet
# Hello!

Notice that "Decorator running for greet" prints immediately when the function is defined, before you even call greet(). The decorator runs once when Python processes the @decorator line.

Call time execution​

The wrapper function runs each time you call the decorated function:

def count_calls(func):
call_count = 0
def wrapper():
nonlocal call_count
call_count += 1
print(f"Function called {call_count} times")
return func()
return wrapper

@count_calls
def greet():
print("Hello!")

greet()
# Output:
# Function called 1 times
# Hello!

greet()
# Output:
# Function called 2 times
# Hello!

The decorator itself (count_calls) runs once at definition time, creating the wrapper. The wrapper runs each time you call greet(), tracking the call count.

Why this matters​

Understanding execution timing helps you avoid common mistakes:

# This won't work as expected
def bad_decorator(func):
# This runs at definition time, not call time
print("Setting up...")
return func

@bad_decorator
def greet():
print("Hello!")

# "Setting up..." already printed
greet() # Just prints "Hello!"

If you need setup code to run at call time, put it in the wrapper, not in the decorator itself.

State in decorators​

Because decorators run at definition time, you can use closures to maintain state across function calls:

def call_counter(func):
count = 0 # This is captured in the closure

@wraps(func)
def wrapper(*args, **kwargs):
nonlocal count
count += 1
print(f"{func.__name__} has been called {count} times")
return func(*args, **kwargs)

return wrapper

@call_counter
def process_data(data):
return data.upper()

process_data("hello") # process_data has been called 1 times
process_data("world") # process_data has been called 2 times

The count variable is created once at definition time and persists across all calls to the decorated function. This is a closure: the wrapper function captures the count variable from the decorator's scope.

Stacking decorators​

You can apply multiple decorators to the same function. They stack from bottom to top:

from functools import wraps

def bold(func):
@wraps(func)
def wrapper():
return f"<b>{func()}</b>"
return wrapper

def italic(func):
@wraps(func)
def wrapper():
return f"<i>{func()}</i>"
return wrapper

@bold
@italic
def greet():
return "Hello"

print(greet())
# Output:
# <b><i>Hello</i></b>

The decorators apply from bottom to top: first italic wraps greet, then bold wraps the result. This is equivalent to:

greet = bold(italic(greet))

Order matters​

The order of decorators affects the result:

@bold
@italic
def text1():
return "Hello"

@italic
@bold
def text2():
return "Hello"

print(text1()) # <b><i>Hello</i></b>
print(text2()) # <i><b>Hello</b></i>

Different order, different result. Think of decorators as layers: the bottom decorator is the innermost layer.

Real-world example: Authentication and logging​

from functools import wraps

def require_auth(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not is_authenticated():
raise PermissionError("Not authenticated")
return func(*args, **kwargs)
return wrapper

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

@require_auth
@log_calls
def delete_user(user_id):
return f"Deleted user {user_id}"

When you call delete_user(), it first checks authentication, then logs the call, then executes the function. The decorators compose together. The order matters: authentication happens before logging, so if authentication fails, the function call is never logged.

Decorators with arguments: The pattern explained​

Decorators that accept arguments require an extra layer: a function that returns a decorator. Understanding this pattern is key to writing flexible decorators.

The three-layer structure​

from functools import wraps

def repeat(times):
# Layer 1: Outer function (takes decorator arguments)
def decorator(func):
# Layer 2: Decorator (takes the function)
@wraps(func)
def wrapper(*args, **kwargs):
# Layer 3: Wrapper (wraps the function call)
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator

@repeat(3)
def greet():
print("Hello!")

greet()
# Output:
# Hello!
# Hello!
# Hello!

Here's what happens step by step:

  1. repeat(3) is called, returning the decorator function
  2. That decorator is applied to greet, returning the wrapper function
  3. When you call greet(), the wrapper executes

This is equivalent to:

decorator = repeat(3)
greet = decorator(greet)

Why the extra layer?​

Without arguments, a decorator is a function that takes a function. With arguments, you need a function that returns a function that takes a function:

# No arguments: decorator(func)
def simple_decorator(func):
return wrapper

# With arguments: decorator(args)(func)
def decorator_with_args(arg):
def actual_decorator(func):
return wrapper
return actual_decorator

The outer function handles the arguments and creates a closure that captures them. The inner function is the actual decorator that uses those captured arguments.

Real-world example: Rate limiting​

from functools import wraps
import time

def rate_limit(calls_per_second):
min_interval = 1.0 / calls_per_second
last_called = [0.0] # Using list to allow modification in closure

def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
elapsed = time.time() - last_called[0]
if elapsed < min_interval:
time.sleep(min_interval - elapsed)
last_called[0] = time.time()
return func(*args, **kwargs)
return wrapper
return decorator

@rate_limit(2) # Max 2 calls per second
def api_call():
print("API called")
return "success"

api_call()
time.sleep(0.3)
api_call() # Will wait to maintain rate limit

The decorator takes calls_per_second as an argument, then returns a decorator that enforces that rate limit. The last_called list is captured in the closure and shared across all calls to the decorated function.

Why functools.wraps matters​

When you create a wrapper function, it replaces the original function. This can cause problems with introspection tools that rely on function metadata.

The problem​

def my_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

@my_decorator
def greet():
"""A greeting function."""
return "Hello!"

print(greet.__name__) # wrapper (not greet!)
print(greet.__doc__) # None (lost the docstring!)
print(help(greet)) # Shows wrapper, not greet

The wrapper function has its own name and metadata, not the original function's. This breaks debugging, documentation, and introspection.

The solution: functools.wraps​

functools.wraps copies the original function's metadata to the wrapper:

from functools import wraps

def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

@my_decorator
def greet():
"""A greeting function."""
return "Hello!"

print(greet.__name__) # greet (preserved!)
print(greet.__doc__) # A greeting function. (preserved!)
print(help(greet)) # Shows greet with its docstring

@wraps(func) is itself a decorator that copies metadata from func to wrapper. It preserves:

  • __name__
  • __doc__
  • __module__
  • __annotations__
  • __qualname__
  • And other attributes

What wraps actually does​

functools.wraps is a decorator that updates the wrapper function's __dict__ with attributes from the original function. It's essentially doing:

wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
wrapper.__module__ = func.__module__
wrapper.__dict__.update(func.__dict__)
# ... and more

But it does it correctly and handles edge cases, so always use wraps instead of doing it manually.

When metadata matters​

Without wraps, debugging becomes harder:

# Without wraps
def timing_decorator(func):
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper

@timing_decorator
def slow_function():
"""This function does something slow."""
time.sleep(1)
return "done"

# In a traceback, you'd see "wrapper" instead of "slow_function"
# Making debugging much harder

With wraps, tracebacks and debugging tools show the correct function name, making problems easier to diagnose.

Class-based decorators​

Decorators don't have to be functions. You can create decorators using classes:

from functools import wraps

class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
wraps(func)(self) # Preserve metadata

def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} called {self.count} times")
return self.func(*args, **kwargs)

@CountCalls
def greet():
print("Hello!")

greet() # greet called 1 times
greet() # greet called 2 times

When you use @CountCalls, Python calls CountCalls(greet), which creates an instance. When you call greet(), Python calls the instance's __call__ method.

Class-based decorators with arguments​

You can also create class-based decorators that accept arguments:

from functools import wraps

class Repeat:
def __init__(self, times):
self.times = times

def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(self.times):
result = func(*args, **kwargs)
return result
return wrapper

@Repeat(3)
def greet():
print("Hello!")

greet()
# Output:
# Hello!
# Hello!
# Hello!

Here, Repeat(3) creates an instance, then that instance is called with greet as an argument, which returns the wrapper.

Summary​

  • Execution timing matters: Decorators run at definition time, wrappers run at call time. This affects when state is created and when code executes.
  • Decorators stack bottom to top: Multiple decorators apply from the bottom up, and order matters.
  • Decorators with arguments need three layers: Outer function (takes arguments), decorator (takes function), wrapper (wraps call).
  • Always use functools.wraps: Preserves function metadata for debugging, documentation, and introspection.
  • Decorators can be classes: Classes can act as decorators by implementing __call__ or __init__ appropriately.

Understanding these advanced concepts helps you write more sophisticated decorators and debug decorator-related issues. For fundamental decorator concepts and common patterns, see the guide on decorators.