Difficulty: Advanced
Decorators are one of Python's most powerful features, allowing you to modify or extend the behavior of functions and methods without changing their source code. At their core, a decorator is simply a function that takes another function as an argument and returns a new function that usually extends the behavior of the original.
The @syntax is syntactic sugar that makes applying decorators clean and readable. When you write @my_decorator above a function definition, Python automatically passes that function to my_decorator() and rebinds the name to whatever my_decorator returns. This is equivalent to writing func = my_decorator(func) after the function definition.
A common problem with decorators is that the wrapper function replaces the original function's metadata (name, docstring, etc.). The functools.wraps decorator solves this by copying the original function's attributes to the wrapper. Always use @functools.wraps(func) inside your decorator to preserve introspection.
Decorators can also accept arguments. To achieve this, you need an extra layer of nesting: an outer function that accepts the decorator arguments and returns the actual decorator function, which in turn accepts and wraps the target function. This three-level pattern (decorator factory -> decorator -> wrapper) is essential for configurable decorators.
Decorators are used extensively throughout Python's ecosystem: @property, @staticmethod, @classmethod in OOP; @app.route in Flask; @login_required in Django. Understanding how to write and compose decorators is a critical skill for any advanced Python developer.
def greet_decorator(func):
def wrapper():
print("Before the function call")
func()
print("After the function call")
return wrapper
@greet_decorator
def say_hello():
print("Hello!")
say_hello()
The @greet_decorator syntax wraps say_hello so that wrapper() runs instead, adding behavior before and after the original call.
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, kwargs):
print("Calling:", func.__name__)
return func(*args, kwargs)
return wrapper
@my_decorator
def add(a, b):
"""Add two numbers."""
return a + b
print(add(3, 4))
print(add.__name__)
print(add.__doc__)
@wraps(func) preserves the original function's __name__ and __doc__ attributes, which is critical for debugging and introspection.
from functools import wraps
def repeat(n):
def decorator(func):
@wraps(func)
def wrapper(*args, kwargs):
for _ in range(n):
result = func(*args, kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
repeat(3) returns the actual decorator. The three-level nesting (repeat -> decorator -> wrapper) allows passing configuration to the decorator.
from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, kwargs):
return "<b>" + func(*args, kwargs) + "</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, kwargs):
return "<i>" + func(*args, kwargs) + "</i>"
return wrapper
@bold
@italic
def format_text(text):
return text
print(format_text("Hello"))
Decorators are applied bottom-up: italic wraps format_text first, then bold wraps the result. This is equivalent to bold(italic(format_text)).
function decorators, @syntax, functools.wraps, decorators with arguments