The @decorator syntax above a function definition is just syntactic sugar: it passes the function being defined into the decorator function, and rebinds the original name to whatever the decorator returns — usually a wrapper function that calls the original plus some extra behavior. This is how libraries add logging, timing, caching, authentication checks, or route registration to a function without touching its internal logic, and it's the same mechanism behind built-in decorators like staticmethod and property.
1Understanding Decorators
The @decorator syntax above a function definition is just syntactic sugar: it passes the function being defined into the decorator function, and rebinds the original name to whatever the decorator returns — usually a wrapper function that calls the original plus some extra behavior. This is how libraries add logging, timing, caching, authentication checks, or route registration to a function without touching its internal logic, and it's the same mechanism behind built-in decorators like staticmethod and property.
Always apply functools.wraps to your wrapper function inside a decorator — without it, the decorated function loses its original name, docstring, and metadata, which breaks introspection tools, help(), and debugging.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
@timer
def slow_add(a, b):
time.sleep(0.1)
return a + b
print(slow_add(2, 3))2Practical Example
Here is a real-world application of Decorators showing how it is used in production Python code.
def require_positive(func):
def wrapper(n):
if n < 0:
raise ValueError("n must be positive")
return func(n)
return wrapper
@require_positive
def square_root(n):
return n ** 0.5
print(square_root(9))3Best Practices
Follow these guidelines when working with Decorators:
1. Use functools.wraps(func) on the inner wrapper function so decorated functions keep their original name and docstring
2. Have the wrapper accept and forward *args and **kwargs so the decorator works on functions with any signature
3. Keep each decorator focused on one cross-cutting concern (timing, logging, caching) so they can be combined and reused independently
Tip: Always apply functools.wraps to your wrapper function inside a decorator — without it, the decorated function loses its original name, docstring, and metadata, which breaks introspection tools, help(), and debugging.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
@timer
def slow_add(a, b):
time.sleep(0.1)
return a + b
print(slow_add(2, 3))