Decorators are one of the most powerful and elegant features in Python. They allow you to modify the behavior of functions and methods in a flexible and reusable way, without changing their original source code. If you've ever used @staticmethod, @classmethod or @property, you've already worked with decorators!
In this complete guide, you'll learn everything from the basics to advanced decorator techniques, with practical examples you can apply right away in your projects.
What Are Decorators?
In Python, a decorator is a function that takes another function as an argument and extends its behavior without explicitly modifying its code. It's like wrapping a "gift" around a function to add extra functionality.
Decorator syntax uses the @ symbol followed by the decorator name, placed right above a function definition:
@my_decorator
def my_function():
pass
This syntax is equivalent to:
def my_function():
pass
my_function = my_decorator(my_function)
Understanding this equivalence is key to grasping how decorators work "under the hood".
Python's Built-in Decorators
Python already comes with several native decorators you can use immediately in your projects.
@staticmethod
The @staticmethod decorator defines a static method within a class. Static methods don't receive the self (instance) or cls (class) parameter, making them useful for functionality that doesn't need to access class attributes.
class Calculator:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def multiply(a, b):
return a * b
Calling without creating an instance
print(Calculator.add(5, 3)) # Output: 8
print(Calculator.multiply(4, 2)) # Output: 8
According to Python's official documentation, static methods are similar to methods in [C++](https://en.wikipedia.org/wiki/C%2B%2B) or [Java](https://www.wikipedia.org/wiki/Java_(programming_language)). The official documentation is available at [python.org](https://docs.python.org/3/library/functions.html#staticmethod).
@classmethod
The @classmethod decorator defines a class method that receives the class as its first argument (conventionally called cls). This allows you to access and modify class attributes, create factory methods, and manipulate the class itself.
class Person:
total_people = 0
def __init__(self, name, age):
self.name = name
self.age = age
Person.total_people += 1
@classmethod
def create_birthday(cls, name):
"""Factory method to create person with 0 years"""
return cls(name, 0)
@classmethod
def get_total(cls):
return cls.total_people
@classmethod
def change_total(cls, new_value):
cls.total_people = new_value
p1 = Person("Ana", 25)
p2 = Person.create_birthday("João")
print(Person.get_total()) # Output: 2
Class methods are especially useful for alternative constructors, as shown in examples from [PEP 8](https://peps.python.org/pep-0008/) and the [official documentation](https://docs.python.org/3/library/functions.html#classmethod).
@property
The @property decorator allows you to define methods that behave like attributes, enabling you to add getter, setter, and deleter logic in a controlled way.
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@property
def fahrenheit(self):
return (self._celsius * 9/5) + 32
@property
def kelvin(self):
return self._celsius + 273.15
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero")
self._celsius = value
temp = Temperature(25)
print(temp.fahrenheit) # Output: 77.0
print(temp.kelvin) # Output: 298.15
temp.celsius = 30
print(temp.celsius) # Output: 30
The @property decorator is essential for encapsulation in Python. More information can be found in the [official Python documentation](https://docs.python.org/3/library/functions.html#property) and the [Python Cookbook](https://www.oreilly.com/library/view/python-cookbook-3rd/9781440597339/).
Creating Your Own Decorator
Now that you know the built-in decorators, let's learn how to create your own!
Simple Decorator
A custom decorator is simply a function that takes a function and returns a new one:
def my_decorator(func):
def wrapper():
print("Before the function")
func()
print("After the function")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Output:
Before the function
Hello!
After the function
Decorator with Arguments
To create decorators that accept arguments, we need one more layer of functions:
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Maria")
Output:
Hello, Maria!
Hello, Maria!
Hello, Maria!
Preserving Function Metadata
A common problem when creating decorators is losing the original function's metadata (like __name__, __doc__, etc.). To preserve it, we use functools.wraps:
import functools
def log_decorator(func):
@functools.wraps(func)
def wrapper(*args, *kwargs):
print(f"Calling {func.name}")
result = func(args, **kwargs)
print(f"{func.name} finished")
return result
return wrapper
@log_decorator
def add(a, b):
"""Adds two numbers"""
return a + b
print(add.name) # Output: add (not wrapper!)
print(add.doc) # Output: Adds two numbers
Without @functools.wraps, the name would be "wrapper" and the docstring would be lost. This technique is explained in the [functools documentation](https://docs.python.org/3/library/functools.html).
Custom Argument Decorators
Let's create a more useful decorator: one that measures function execution time:
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, *kwargs):
start = time.time()
result = func(args, **kwargs)
end = time.time()
print(f"{func.name} ran in {end - start:.4f} seconds")
return result
return wrapper
@timer
def process_data(lst):
total = 0
for item in lst:
total += item ** 2
return total
result = process_data(range(1000))
print(f"Result: {result}")
Retry Decorator
Another useful example: decorator that tries to execute the function again if it fails:
import functools
import time
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, *kwargs):
for attempt in range(max_attempts):
try:
return func(args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
print(f"Attempt {attempt + 1} failed: {e}")
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=1)
def connect_api():
import random
if random.random() > 0.7:
return "Connection established!"
raise ConnectionError("API unavailable")
print(connect_api())
Decorators for Classes
You can also apply decorators to classes! A famous example is the @dataclass decorator from the dataclasses module:
from dataclasses import dataclass
from typing import List
@dataclass
class Product:
name: str
price: float
quantity: int = 0
def total(self):
return self.price * self.quantity
product = Product("Notebook", 2500.00, 3)
print(product)
print(f"Total: R$ {product.total()}")
Output:
Product(name='Notebook', price=2500.0, quantity=3)
Total: R$ 7500.0
There are also decorators like @singledispatch (for overloaded functions), @lru_cache (for memoization), and more. Complete documentation is available at [docs.python.org](https://docs.python.org/3/library/functools.html).
Decorators with State
We can create decorators that maintain state between calls using closures:
import functools
def cache(func):
@functools.wraps(func)
def wrapper(args):
if args not in wrapper.cache:
wrapper.cache[args] = func(args)
return wrapper.cache[args]
wrapper.cache = {}
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(100)) # Calculated once
print(fibonacci(100)) # Retrieved from cache
print(fibonacci(50)) # Calculated
print(fibonacci(50)) # Retrieved from cache
Decorator Stacks
You can apply multiple decorators to the same function. They are applied from bottom to top:
import functools
def decorator_a(func):
@functools.wraps(func)
def wrapper(*args, *kwargs):
print("A - Before")
result = func(args, **kwargs)
print("A - After")
return result
return wrapper
def decorator_b(func):
@functools.wraps(func)
def wrapper(*args, *kwargs):
print("B - Before")
result = func(args, **kwargs)
print("B - After")
return result
return wrapper
@decorator_a
@decorator_b
def my_function():
print("My function running")
my_function()
Output:
A - Before
B - Before
My function running
B - After
A - After
Classes as Decorators
Besides functions, you can use classes as decorators by implementing the __call__ method:
import functools
class ClassDecorator:
def init(self, func):
self.func = func
functools.update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print("Before the call")
result = self.func(*args, **kwargs)
print("After the call")
return result
@ClassDecorator
def test_function():
print("Function executing")
test_function()
Real Usage: Flask and Django
Decorators are widely used in web frameworks. In Flask, for example:
from functools import wraps
def login_required(f):
@wraps(f)
def decorated_function(*args, *kwargs):
if not current_user.is_authenticated:
return redirect(url_for('login'))
return f(args, **kwargs)
return decorated_function
@app.route('/profile')
@login_required
def profile():
return render_template('profile.html')
In Django, decorators are used for permission and authentication control. More information about decorators in frameworks can be found in the [Flask documentation](https://flask.palletsprojects.com/) and [Django](https://docs.djangoproject.com/).
Best Practices
Now that you master decorators, here are some best practices:
1. Always use @functools.wraps
import functools
def my_decorator(func):
@functools.wraps(func) # Preserve metadata
def wrapper(*args, *kwargs):
return func(args, **kwargs)
return wrapper
2. Document your decorators
defdeprecated(reason):
"""Decorator to mark functions as deprecated.
Args:
reason: String explaining why the function is deprecated.
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
import warnings
warnings.warn(
f"{func.__name__} is deprecated: {reason}",
DeprecationWarning,
stacklevel=2
)
return func(*args, **kwargs)
return wrapper
return decorator
@deprecated("Use new_function instead")
def old_function():
pass
3. Use *args and **kwargs
def validate_args(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Validate arguments before
return func(*args, **kwargs)
return wrapper
4. Be careful with decorators on instance methods
class MyClass:
@my_decorator
def method(self):
# The decorator should use *args, **kwargs
pass
Advanced Decorators
Decorators with configuration options
import functools
def configure(*config):
def decorator(func):
@functools.wraps(func)
def wrapper(args, **kwargs):
if config.get('log', False):
print(f"Running {func.name}")
if config.get('cache', False):
Implement cache
pass
return func(*args, **kwargs)
return wrapper
return decorator
@configure(log=True, cache=True)
def process():
pass
Type validation decorator
import functools
def checktypes(*types):
def decorator(func):
@functools.wraps(func)
def wrapper(args, **kwargs):
for name, type in types.items():
if name in kwargs:
if not isinstance(kwargs[name], type):
raise TypeError(
f"{name} must be {type.name}, "
f"received {type(kwargs[name]).name}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@check_types(age=int, name=str)
def create_user(name, age):
return f"User {name}, {age} years old"
print(create_user(name="João", age=25)) # OK
print(create_user(name="João", age="25")) # TypeError
When to Use Decorators
Decorators are ideal for:
- Logging: Record function calls
- Authentication and authorization: Check permissions
- Caching: Store results of expensive functions
- Validation: Verify arguments before execution
- Instrumentation: Measure execution time
- Retry: Try again on failure
Decorators vs Wrappers
It's important to understand the difference:
Decorators are the @decorator syntax applied to functions or classes. They are the general concept.
Wrappers (or wrappers) are the inner functions that receive the original function and add behavior.
In the example:
def decorator(func): # "decorator" is the decorator function
def wrapper(*args, **kwargs): # "wrapper" is the wrapper
return func(*args, **kwargs)
return wrapper
Next Steps
Now that you master decorators, keep learning:
- Python Generators — learn about generators and iterators
- Context Managers — manage resources with the context manager protocol
Decorators are fundamental for writing Pythonic and reusable code. Practice creating your own decorators and you'll see a significant improvement in your code quality!
For more content about Python and development, keep following Universo Python!