Demystifying Python Decorators: A Guide for Developers
If you have spent any time reading modern Python frameworks like Flask or Django, you have likely encountered the @ symbol sitting atop function definitions. These are decorators, a powerful feature that allows you to modify the behavior of a function or class without permanently changing its source code.
In this post, we will break down how they work and how to write your own.
Functions as First-Class Objects
To understand decorators, you first need to understand that in Python, functions are first-class objects. This means they can be passed around and used as arguments, just like strings or integers.
def say_hello(name):
return f"Hello, {name}!"
def greet_bob(greeter_func):
return greeter_func("Bob")
# We pass the function object, not the result of the function
print(greet_bob(say_hello))
# Output: Hello, Bob!
Building a Simple Decorator
At its core, a decorator is simply a function that takes another function and extends the behavior of the latter without explicitly modifying it.
Here is a manual implementation without the @ syntax:
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper
def say_whee():
print("Whee!")
# Manually decorating
say_whee = my_decorator(say_whee)
say_whee()
The Syntactic Sugar
Python allows you to use the @ symbol to apply the decorator automatically. This is syntactic sugar for say_whee = my_decorator(say_whee).
@my_decorator
def say_whee():
print("Whee!")
say_whee()
Real-World Use Case: Timing Functions
Decorators are incredibly useful for cross-cutting concerns like logging, access control, or measuring execution time. Here is a practical example of a timer decorator:
import time
import functools
def timer(func):
@functools.wraps(func) # Preserves the metadata of the original 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__!r} in {run_time:.4f} secs")
return value
return wrapper_timer
@timer
def waste_some_time(num_times):
for _ in range(num_times):
sum([i**2 for i in range(10000)])
waste_some_time(1)
Conclusion
Decorators are a clean and Pythonic way to separate core logic from auxiliary functionality. By mastering them, you can write code that is modular, readable, and easier to maintain.

