Python Functions
Python Decorators
A decorator is a function that wraps another function to add extra behaviour — like logging, timing, or access checks — without changing the original function's code.
How Decorators Work
A decorator is just a function that takes another function as input, defines a wrapper function around it, and returns that wrapper. Because it relies on an inner function capturing the original one, decorators are a direct, practical use of closures.
def shout(func):
def wrapper():
result = func()
return result.upper()
return wrapper
def greet():
return "hello"
greet = shout(greet) # manually wrapping
print(greet()) # HELLOThe @ Syntax
Writing @decorator_name directly above a function definition is shorthand for the manual wrapping shown above — Python applies it automatically when the function is defined.
def shout(func):
def wrapper():
return func().upper()
return wrapper
@shout
def greet():
return "hello"
print(greet()) # HELLOHandling Arguments with *args and **kwargs
Real functions usually take arguments, so a general-purpose decorator's wrapper should accept and forward any arguments using *args and **kwargs.
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
@timer
def slow_add(a, b):
time.sleep(0.2)
return a + b
print(slow_add(3, 4))Why functools.wraps Matters
Without functools.wraps, the wrapped function loses its original name and docstring — func.__name__ becomes "wrapper" for every decorated function, which makes debugging and introspection confusing. @functools.wraps(func) fixes this by copying that metadata over.
Always add @functools.wraps(func) inside your decorator's wrapper — skipping it is a very common beginner mistake that breaks help() and debugging tools.
A Logging Decorator Example
import functools
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}{args}")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a, b):
return a + b
add(2, 3) # Calling add(2, 3)