Mastering decorators in Python: a complete guide with examples
Decorators in Python are fascinating and powerful features that can improve your code. They allow you to modify or extend the behavior of

Decorators in Python are fascinating and powerful features that can improve your code. They allow you to modify or extend the behavior of functions and methods without changing the original code. By using them, you can add features to existing functions in a fluid and sustainable way, making it more readable and efficient.
In this article, we'll explore:
- How functions work in Python;
- The concept of decorators;
- How decorators work in Python;
- Practical examples to illustrate the usefulness of decorators.
Whether you're an experienced Python developer or someone who's just starting out, understanding decorators can open up new possibilities in your programming skill set.
Let's get started!
How functions work in Python
In Python, functions are first-class objects. This means you can work with them the same way you work with other objects, such as strings, integers, or lists. The fact that they are first-class objects gives your functions powerful properties:
- A function is an instance of the Object type;
- It can be stored in a variable;
- It can be passed as a parameter to another function;
- It can be returned from another function;
- It can be stored in data structures such as lists or dictionaries.
To better understand functions, let's create some examples:
def add_numbers(number1, number2):
return number1 + number2
def subtract_numbers(number1, number2):
return number1 - number2
def apply_function(operation_func):
return operation_func(10, 5)
print(f"Addition: {apply_function(add_numbers)}")
print(f"Subtraction: {apply_function(subtract_numbers)}")Running the Python script we created, we'll get the following result:
Addition: 15
Subtraction: 5In this example, we created two regular functions, add_numbers and subtract_numbers, which take two numbers as input. We also created another function, apply_function, which accepts a function as an argument. Then, we used apply_function twice, first with add_numbers and then with subtract_numbers.
Here's an important distinction to note. When we pass add_numbers to apply_function, we use add_numbers without parentheses. This means we're passing a reference to the function itself, rather than calling it directly. On the other hand, when we write apply_function(...) with parentheses, we're actively calling the function, and it executes as usual.
This demonstrates the concept of functions as first-class objects. A function without parentheses is just a reference, like a variable. But when you add parentheses, it's executed, returning its result.
Inner function: defining functions inside others
It's also possible to define functions inside other functions, which is called inner-functions, and we'll soon see how this is relevant to creating decorators in Python.
Let's create a new example with two inner functions:
def greetings(name):
def hello():
return "Hello"
def say_something_nice():
return "Have a nice day!"
return f"{hello()} {name}. {say_something_nice()}"
print(greetings("John"))Running this, you'll get the following output:
Hello John. Have a nice day!Inside the greetings function, we created two other functions, hello and say_something_nice. They're called inner functions because they're defined inside another function.
It's important to understand that inner functions have local scope to their outer function. This means you can't call an inner function directly unless the outer one has already been called. If you try to run hello outside of greetings, you'll get an error.
Here's an example where we modify the code to call hello outside the greetings function to see what happens.
def greetings(name):
def hello():
return "Hello"
def say_something_nice():
return "Have a nice day!"
return f"{hello()} {name}. {say_something_nice()}"
print(greetings("John"))
print(hello())Now, when you run it, you'll get this output:
Hello John. Have a nice day!
Traceback (most recent call last):
. . .
NameError: name 'hello' is not definedYou can see that the function runs correctly when you call the outer one (greetings), but if you try to run just the inner function hello, you'll get an error. This happens because of the local scope of the inner function, which isn't available outside the outer one.
Functions that return other functions
Functions can also return other functions. Here's a clearer example:
def classify_number(number):
def even():
return "Number is even"
def odd():
return "Number is odd"
if number % 2 == 0:
return even
else:
return odd
number = 10
result = classify_number(number)
print(f"Return from classify_number: {result}")
print(f"Executing the function returned by classify_number: {result()}")
print("\n")
number = 11
result = classify_number(number)
print(f"Return from classify_number: {result}")
print(f"Executing the function returned by classify_number: {result()}")When you run this code, the output will be:
Return from classify_number: <function classify_number.<locals>.even at 0x104beb040>
Executing the function returned by classify_number: Number is even
Return from classify_number: <function classify_number.<locals>.odd at 0x104beb160>
Executing the function returned by classify_number: Number is oddExplanation
- The
classify_numberfunction contains two inner functions:evenandodd, which return a text (string) object that describes the type of number. - Depending on whether the input number is even or odd, the
classify_numberfunction returns a reference to theevenoroddfunction. It doesn't execute them yet, it just provides a reference. - Once the returned function (
result) is stored, you can call it later, likeresult(), to execute it and get the appropriate message.
By returning a reference to the function, you have the flexibility to call it when you need to, instead of getting the result immediately during the initial function call.
Getting started with decorators in Python
Now that we understand how functions work in Python, let's learn about decorators and why they're so powerful. A decorator is essentially a function that modifies the behavior of another function.
Here's a simple example:
def decorator(func):
def wrapper(*args, **kwargs):
print(f"Function {func.__name__} is called")
func(*args, **kwargs)
print(f"Function {func.__name__} is completed")
return wrapper
def add_numbers(number1, number2):
print(number1 + number2)
add_numbers = decorator(add_numbers)Explanation
- We created two functions:
decorator: the decorator function.add_numbers: function that prints the sum of two numbers.
- Inside
decorator, we define an inner function calledwrapper.- This function accepts any arguments (
*args) and keyword arguments (**kwargs), which can be passed to the original function. - It prints a message before and after calling the original function.
- This function accepts any arguments (
- We apply the decorator to
add_numbersby reassigningadd_numbersto the result ofdecorator(add_numbers). Now,add_numberspoints to thewrapperfunction.
When run in a Python shell:
>>> from decorator1 import add_numbers
>>> add_numbers(10, 5)This will produce:
Function add_numbers is called
15
Function add_numbers is completed
What's happening
- The
add_numbersfunction now refers to thewrapperfunction. - The
wrapper, in turn, calls the originaladd_numbersfunction (the one we defined initially) and adds the extra functionality (printing messages) before and after its execution.
>>> from decorator1 import add_numbers
>>> add_numbers
<function decorator.<locals>.wrapper at 0x104f6c700>You know that a decorator basically wraps a function, modifying its behavior. But the way we applied the decorator in the previous example is peculiar. Below is a more suitable alternative:
The Pythonic way of using decorators
The way we applied the decorator above works, but there's a cleaner, more Pythonic approach using the @ symbol (called "pie syntax"). Using it, our example looks like this:
def decorator(func):
def wrapper(*args, **kwargs):
print(f"Function {func.__name__} is called")
func(*args, **kwargs)
print(f"Function {func.__name__} is completed")
return wrapper
@decorator
def add_numbers(number1, number2):
print(number1 + number2)Here's what changed:
- By adding
@decoratorabove theadd_numbersfunction, we automatically apply thedecoratorwithout needing to manually reassignadd_numbers. - The behavior stays the same, but the code becomes much cleaner and easier to read.
Overall, this demonstrates how decorators let you wrap a function with additional behavior in an elegant and reusable way
Real-world examples of Python decorators
Now that we know how to create decorators, let's look at some practical examples to understand how they can be useful in real-world scenarios.
Example 1: measuring execution time
A common use of decorators is measuring how long a function takes to run. This can be especially useful for performance optimization. Here's how we can create this decorator:
import time
def execution_timer(func):
"""Print the runtime of the decorated function"""
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__}() in {run_time:.4f} secs")
return value
return wrapper_timerNow, let's use this decorator with a function that takes time to run and check the output:
>>> from execution_timer_decorator import execution_timer
>>> @execution_timer
... def amazing_function():
... for _ in range(500):
... sum([number*number for number in range(10000)])
...
>>> amazing_function()
Finished amazing_function() in 0.1985 secsHow it works
- The decorator stores the start time (
start_time) right before the function runs. - After the function finishes, it calculates the total execution time (
run_time) by subtractingstart_timefrom the end time. - Finally, it prints the execution time to the terminal console.
This can help you identify slow functions in your code.
Example 2: checking headers in a Django view
Another useful decorator can check whether a request contains specific headers before the endpoint processes it. For example, you might want to ensure a request contains certain headers before accepting it.
Here's how you can create this decorator:
import logging
from functools import wraps
from typing import List
from rest_framework import status
from rest_framework.response import Response
def request_has_headers(headers_names: List[str]):
def decorator(function):
@wraps(function)
def wrapper(request, *args, **kwargs):
for header in headers_names:
if header.capitalize() not in request.headers:
return Response(status=status.HTTP_400_BAD_REQUEST, data=f"Header {header} is missing")
return function(request, *args, **kwargs)
return wrapper
return decoratorUsing the decorator in a Django View
Now you can use this decorator to check the headers without overloading your view's logic:
@method_decorator(request_has_headers(["my-first-header", “my-second-header]))
def list(self, request):
my_data = MyModel.objects.all()
return Response(self.serializer_class(my_data, many=True, context={"request": request}).data, status=status.HTTP_200_OK)How it works
- Pass a list of required headers (
["my-first-header", "my-second-header"]) to the decorator. - The decorator goes through the required headers and checks whether each one is present in the request headers.
- If a header is missing, it immediately returns a
400 Bad Requestresponse with an error message. - If all headers are present, the original function (for example, the Django view) runs.
This approach can help ensure your requests meet specific requirements, without overloading the view with repetitive checks.
Conclusion
In this article, we explored how functions in Python can be passed like other objects, how to define inner functions, and how to use and create decorators. Decorators are a powerful and flexible feature in Python that let you modify the behavior of functions or methods without changing their source code. They make it easy to add reusable functionality, such as measuring execution time or validating requests, while keeping your code clean and organized. Overall, it's an incredibly useful tool in Python development.
Translated from the Brazilian Portuguese original · Read the original





