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:

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!