In Post #140, we built a practical @timer
decorator to measure a function’s execution time. This demonstrated how decorators can add functionality to a function without modifying its source code. Another extremely common and useful application for decorators is logging.
In this post, we’ll build a @logger
decorator. This tool will help us with debugging by automatically printing information about a function call: its name, the arguments it received, and the value it returned.
The Goal: A Debugging Helper
When you’re debugging a complex program, you often find yourself adding print()
statements at the beginning and end of a function just to see what’s going on. A @logger
decorator can automate this process for us.
Our goal is to create a decorator that, when applied to any function, will automatically print:
- The name of the function being executed.
- The positional (
*args
) and keyword (**kwargs
) arguments it was called with. - The value it returned.
Building the @logger
Decorator
We will use the same robust decorator pattern from our last post, which handles any combination of arguments and correctly passes along the function’s return value.
def logger(func):
"""A decorator that logs a function's execution details."""
def wrapper(*args, **kwargs):
# 1. Log the function call and its arguments
print(f"--- Calling function '{func.__name__}' ---")
print(f"Positional arguments (args): {args}")
print(f"Keyword arguments (kwargs): {kwargs}")
# 2. Call the original function and store the result
result = func(*args, **kwargs)
# 3. Log the return value
print(f"Function '{func.__name__}' returned: {result}")
print(f"--- Finished function '{func.__name__}' ---\n")
# 4. Return the original result, so the caller gets it
return result
return wrapper
Using the @logger
Decorator
Now, we can apply our new @logger
to any function to get a detailed trace of its execution. Let’s create a simple function that adds two numbers.
@logger
def add(x, y):
"""Adds two numbers together."""
return x + y
# Call the decorated function
add(5, 3)
Without any print
statements inside our add
function, we get this wonderfully detailed output from the decorator:
--- Calling function 'add' ---
Positional arguments (args): (5, 3)
Keyword arguments (kwargs): {}
Function 'add' returned: 8
--- Finished function 'add' ---
Let’s try it on another function that uses different types of arguments to prove its flexibility.
@logger
def greet(name, greeting="Hello"):
"""Returns a greeting string."""
return f"{greeting}, {name}!"
# Call with both a positional and a keyword argument
greet("World", greeting="Hi")
The output correctly shows how the arguments were received:
--- Calling function 'greet' ---
Positional arguments (args): ('World',)
Keyword arguments (kwargs): {'greeting': 'Hi'}
Function 'greet' returned: Hi, World!
--- Finished function 'greet' ---
What’s Next?
The @logger
decorator is an incredibly useful debugging tool. With a single line (@logger
), you can get a detailed trace of what a function is receiving and what it’s returning, without having to fill your function’s core logic with temporary print()
statements. This is another perfect example of how decorators help separate concerns and keep your code clean.
We’ve now explored several “Pythonic” patterns, from comprehensions to decorators. It’s time to put these new, more elegant techniques into practice. In Post #142, we will have our final project for Part 5: we’ll take a poorly written, C-style script and refactor it into a clean, readable, and Pythonic program.
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