Python Function Decorator: Syntax and Practical Use
python function decorator: Learn how Python function decorators work, how to write them, and when to use them for logging, timing, and access control.
A Python function decorator is a callable that takes a function as input and returns a new function, usually extending its behavior without modifying the original source. The @ syntax is syntactic sugar for applying the decorator to the function defined below it. For example, @timer above a function definition is equivalent to func = timer(func). This pattern is widely used for logging, timing, access control, and input validation.
How a Decorator Works Under the Hood
The core mechanism is simple: a decorator is a function that receives the original function and returns a replacement. The replacement typically calls the original function, but can also run code before or after it.
def logger(func): def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") result = func(*args, **kwargs) print(f"Finished {func.__name__}") return result return wrapper @logger def greet(name): return f"Hello, {name}"
Here, greet is replaced by wrapper. When you call greet("Alice"), the wrapper logs the call, invokes the original function, logs completion, and returns the result. The original function object is no longer directly accessible under the name greet.
Preserving Function Metadata with functools.wraps
The wrapper above has a problem: it loses the original function's __name__, __doc__, and other metadata. This breaks debugging and tools that rely on introspection, such as documentation generators and test runners.
import functools def logger(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") result = func(*args, **kwargs) print(f"Finished {func.__name__}") return result return wrapper
functools.wraps copies __name__, __doc__, __module__, and __dict__ from the original function to the wrapper. It also updates the wrapper's __wrapped__ attribute to point to the original function, which enables further introspection and even unwrapping in some debugging tools. Always use functools.wraps when writing decorators that return a wrapper function.
Decorators That Take Arguments
Sometimes a decorator needs configuration, such as a log level or a retry count. In that case, you need a decorator factory: a function that returns the actual decorator.
def repeat(times): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for _ in range(times): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def say_hi(): print("hi")
The @repeat(3) syntax calls repeat(3), which returns decorator. That decorator is then applied to say_hi. This pattern is common for decorators like @retry(max_attempts=3) or @cache(ttl=60). The outer function captures the arguments, and the inner layers handle the function wrapping.
Class-Based Decorators
Decorators can also be implemented as classes. A class that defines __call__ is callable, so it can be used as a decorator. This is useful when the decorator needs to maintain state across calls.
class Counter: def __init__(self, func): self.func = func self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 print(f"Call {self.count} of {self.func.__name__}") return self.func(*args, **kwargs) @Counter def process(): return "done"
Here, process is replaced by an instance of Counter. Each call increments count. Class-based decorators are convenient when you need to store per-function state, but they can be less intuitive than function-based ones. They also require functools.wraps if you want to preserve metadata, though functools.update_wrapper can be used in __init__.
Stacking Decorators and Order of Application
You can apply multiple decorators to a single function. The decorators are applied from bottom to top, meaning the one closest to the function runs first.
@logger @timer def compute(): pass
This is equivalent to compute = logger(timer(compute)). The timer decorator wraps compute first, then logger wraps the result. When you call compute(), the logger wrapper runs first, then the timer wrapper, and finally the original function. This order matters when decorators depend on each other's behavior. For example, if logger expects to see the function's __name__, it will see the name of the wrapper returned by timer unless timer uses functools.wraps. Stacking many decorators can reduce readability, so keep the chain short and document the order.
Performance and Overhead Considerations
Every decorator adds an extra call layer. For most applications, this overhead is negligible compared to the work the function itself performs. However, if a decorated function is called millions of times in a tight loop, the extra function call can become measurable. The overhead comes from the wrapper's *args, **kwargs packing and unpacking, which is more expensive than a direct call.
You can reduce this by writing decorators that preserve the original function's signature using functools.wraps, but that does not eliminate the call overhead. If performance is critical, consider whether the decorator is necessary in the hot path. For example, a logging decorator that writes to a file on every call will dominate the cost anyway. In such cases, the decorator's overhead is irrelevant. The real performance concern is the work inside the wrapper, not the wrapper itself.
Common Pitfalls: Decorating Methods
When you decorate a method inside a class, the wrapper receives self as the first argument. This works fine if your wrapper accepts *args, **kwargs, but you must be careful when the decorator itself needs to access instance attributes.
def requires_permission(permission): def decorator(func): @functools.wraps(func) def wrapper(self, *args, **kwargs): if not self.user.has_permission(permission): raise PermissionError() return func(self, *args, **kwargs) return wrapper return decorator class API: @requires_permission("admin") def delete_user(self, user_id): pass
The wrapper explicitly takes self as the first argument. This is a common pattern for access control. However, if you write a generic decorator that should work on both functions and methods, you need to handle the case where the first argument is self or cls. A simple approach is to use *args, **kwargs and inspect args[0] if needed. Another pitfall is forgetting to call functools.wraps on the wrapper, which breaks introspection and can confuse debugging tools. Always apply functools.wraps in the innermost wrapper to preserve the original function's metadata.