In our last few posts (Posts #137 Post #138 Post #139), we’ve built up the theory and syntax of decorators, from understanding functions as objects to using the clean @
syntax. Now it’s time to put that knowledge to practical use and build a decorator that does something genuinely useful.
In this post, we will create a @timer
decorator. This decorator can be applied to any function in our code to measure and print how long it takes to execute, a common task when optimizing code for performance.
The Tool: Python’s time
Module
To measure elapsed time, we need a way to get the current time. Python’s built-in time
module is perfect for this. We’ll use the time.time()
function, which returns the number of seconds that have passed since a fixed point in time (known as the “epoch”).
The basic idea is simple: we’ll record the time before our function runs, record it again after it finishes, and the difference between the two is the execution duration.
A More Flexible Wrapper: Handling Arguments and Return Values
Our simple decorator from the last post had a major flaw: it could only decorate a function that took no arguments and returned nothing. A real-world decorator needs to be a universal wrapper that can work with any function.
To make our wrapper accept any arguments, we use the *args
and **kwargs
syntax in its definition. This allows it to accept any combination of positional and keyword arguments. The wrapper must then pass these arguments along to the original function when it calls it.
Furthermore, if the original function returns a value, our wrapper needs to capture that value and return it as well.
The generic pattern for a robust decorator wrapper looks like this:
def generic_decorator(func):
def wrapper(*args, **kwargs):
# 1. Do something before
return_value = func(*args, **kwargs) # 2. Call the original function, passing along arguments
# 3. Do something after
return return_value # 4. Return the original function's return value
return wrapper
Building the @timer
Decorator
Now let’s combine these ideas to build our @timer
. It will record the start time, run the original function, record the end time, print the duration, and then return whatever the original function returned.
import time
def timer(func):
"""A decorator that prints the execution time of a function."""
def wrapper(*args, **kwargs):
# 1. Record the start time
start_time = time.time()
# 2. Call the original function and store its result
result = func(*args, **kwargs)
# 3. Record the end time and calculate the duration
end_time = time.time()
duration = end_time - start_time
# 4. Print the duration and return the original result
# 'func.__name__' is a special attribute that holds the function's name
print(f"Function '{func.__name__}' executed in {duration:.4f} seconds")
return result
return wrapper
Using the @timer
Decorator
Now for the fun part! We can apply this decorator to any function to see how long it takes to run. Let’s create a “wasteful” function that does some work so we have something to measure.
@timer
def wasteful_function(num_times):
"""A simple function that does a lot of work."""
total = 0
for i in range(num_times):
total += i
return total
# Call the decorated function as usual
final_total = wasteful_function(10_000_000)
print(f"The final total is: {final_total}")
When you run this script, you’ll see output similar to this:
Function 'wasteful_function' executed in 0.3528 seconds
The final total is: 49999995000000
The beauty of this is that our wasteful_function
contains only its core logic (summing numbers). The timing logic is completely separate and lives in the decorator. We can add or remove the timing behavior just by adding or removing the @timer
line, without ever touching the function itself.
What’s Next?
You’ve now built your first practical decorator! The @timer
is a great example of how decorators can add functionality (like logging, timing, or caching) to your existing functions without cluttering their internal logic. You also learned the critical *args, **kwargs
pattern for creating decorators that work with any function.
Timing is just one of many useful “cross-cutting concerns” we can handle with decorators. Another is logging—printing messages to track when functions are called and what they are called with. In Post #141, we will build another practical example: a @logger
decorator.
Author

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins. In my free time, I write blogs on ckdbtech.com