Python Decorators Explained with Examples
A clear and practical guide to understanding decorators in Python. Learn what they are, how they work, and how to use them to add functionality to your functions without modifying their code.
If you've spent some time with Python, you've likely seen the @
symbol placed on top of a function definition. This is the syntax for a decorator, one of Python's most powerful features. Decorators are a way to modify or enhance the behavior of a function without permanently changing its source code.
At their core, decorators are just functions that take another function as an argument, add some functionality, and then return another function.
Functions are First-Class Objects
To understand decorators, you first need to understand that in Python, functions are first-class objects. This means you can:
- Assign a function to a variable.
- Pass a function as an argument to another function.
- Return a function from another function.
This is the key concept that makes decorators possible.
A Simple Decorator from Scratch
Let's build a simple decorator that prints a message before and after a function is called.
# This is our decorator function
def my_decorator(func):
# This is the new function that will be returned
def wrapper():
print("Something is happening before the function is called.")
func() # Call the original function
print("Something is happening after the function is called.")
return wrapper
# This is the function we want to decorate
def say_hello():
print("Hello!")
# Now, let's decorate it manually
say_hello_decorated = my_decorator(say_hello)
# Call the new, decorated function
say_hello_decorated()
Output:
Something is happening before the function is called.
Hello!
Something is happening after the function is called.
As you can see, my_decorator
took our say_hello
function, wrapped it in some extra logic inside the wrapper
function, and returned the wrapper
function.
The @
Syntactic Sugar
The manual decoration process above is a bit clumsy. Python provides a much cleaner way to do this with the @
symbol. This is just syntactic sugar for the process we just did manually.
@my_decorator
def say_hello():
print("Hello!")
# Now, when you call say_hello, you are actually calling the decorated version
say_hello()
This code is exactly equivalent to the previous example. The @my_decorator
line is just a shortcut for say_hello = my_decorator(say_hello)
.
A Practical Example: A Timing Decorator
A common use case for decorators is to time how long a function takes to execute.
import time
def timing_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs) # Call the original function, passing arguments
end_time = time.time()
print(f"{func.__name__} took {end_time - start_time:.4f} seconds to run.")
return result
return wrapper
@timing_decorator
def slow_function(delay):
print(f"Sleeping for {delay} seconds...")
time.sleep(delay)
return "Done"
slow_function(2)
Output:
Sleeping for 2 seconds...
slow_function took 2.0012 seconds to run.
Notice the use of *args
and **kwargs
. This is important because it allows our wrapper
function to accept any number of positional or keyword arguments and pass them on to the original function. This makes our decorator generic and reusable for any function.
Why Use Decorators?
- Don't Repeat Yourself (DRY): Decorators allow you to add the same piece of functionality (like logging, timing, or authentication checks) to multiple functions without duplicating code.
- Separation of Concerns: You can keep your core business logic in your function clean and separate from cross-cutting concerns like logging or caching.
- Extensibility: You can easily add or remove functionality from a function just by adding or removing a decorator line, without touching the function's code itself.
Popular web frameworks like Flask and FastAPI use decorators extensively to map functions to URL routes:
@app.route('/users/{user_id}')
def get_user(user_id):
# ... logic to fetch user
Conclusion
Decorators are a powerful and elegant feature of the Python language. While they might seem like magic at first, they are just a clean and reusable application of the principle that functions are first-class objects. By mastering decorators, you can write more modular, readable, and maintainable Python code.