How to Build a Simple Decorator That Logs Function Calls in Python
This code shows how to create a reusable decorator that logs each function call, including arguments, return value, and execution time.
Python code
26 linesimport functools
import time
def log_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} returned {result} in {end - start:.4f}s")
return result
return wrapper
@log_calls
def add(a, b):
return a + b
@log_calls
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
if __name__ == "__main__":
add(2, 3)
greet("Alice")
greet("Bob", greeting="Hi")
Output
Calling add with args=(2, 3), kwargs={}
add returned 5 in 0.0000s
Calling greet with args=('Alice',), kwargs={}
greet returned Hello, Alice! in 0.0000s
Calling greet with args=('Bob',), kwargs={'greeting': 'Hi'}
greet returned Hi, Bob! in 0.0000s
How it works
The log_calls decorator wraps the original function with a wrapper that logs before and after the call. functools.wraps copies metadata like __name__ from the original function so debugging stays clean. The wrapper accepts *args and **kwargs to handle any function signature. Timing uses time.time() around the actual call to measure execution. The decorator returns the original result so behavior remains unchanged.
Common mistakes
- Forgetting `functools.wraps`, which breaks introspection and `__name__` on the decorated function.
- Not returning the result from the wrapper, silently swallowing the function's return value.
- Hardcoding argument names instead of using `*args` and `**kwargs` for generality.
Variations
- Use `functools.partial` or a decorator factory to accept extra options like log level.
- Write the logs to a file or `logging` module instead of `print` for production use.
Real-world use cases
- Adding lightweight logging to API endpoints to track incoming requests and response times.
- Instrumenting database queries to spot slow calls in a batch processing script.
- Debugging a complex algorithm by tracing input/output for each recursive call during development.
Sponsored
More from Functions & basics
- Add Type Hints to Function Parameters and Return in Python easy
- Benchmark list append vs comprehension in Python easy
- Build a Context Manager in Python with contextlib.contextmanager easy
- Build a Progress Callback Function for Loops in Python easy
- Cache expensive function with lru_cache in Python easy
- Calculate Time Difference Across Time Zones in Python easy
Keep learning
Related tutorials and quizzes for this topic.