The @tracer decorator is a debugging and monitoring tool that allows you to trace the execution of functions in Python. Its main purposes are:

  • Function Execution Tracking: It helps monitor when functions are called, what arguments they receive, and what values they return.
  • Debugging Aid: It can print out detailed information about function calls, making it easier to debug complex programs.

Here’s an example implementation of a basic @tracer decorator:

# tracer_example.py
def tracer(func):
    def wrapper(*args, **kwargs):
        # Print function entry
        print(f"Entering function: {func.__name__}")
        print(f"Arguments: args={args}, kwargs={kwargs}")

        # Execute the function
        result = func(*args, **kwargs)

        # Print function exit
        print(f"Exiting function: {func.__name__}")
        print(f"Return value: {result}")

        return result
    return wrapper

# Example usage
@tracer
def greet(name):
    return f"Hello, {name}!"

# When you call greet("Alice"), you'll see the tracing output
result = greet("Alice")

When you use this decorator, it will output something like:

Entering function: greet
Arguments: args=('Alice',), kwargs={}
Exiting function: greet
Return value: Hello, Alice!

Key benefits of using the @tracer decorator:

  • Non-invasive Debugging: You can add and remove the decorator without changing the function’s code.
  • Consistent Logging: Provides a standardized way to log function calls across your codebase.
  • Performance Profiling: Can be extended to measure function execution time.
  • Call Stack Analysis: Helps understand the flow of execution in your program.

You can also create more sophisticated tracers that:

  • Track recursion depth
  • Measure execution time
  • Log to files instead of printing
  • Add indentation to show call hierarchy
  • Filter certain types of functions or arguments

The @tracer decorator is particularly useful during development and debugging phases, but it’s typically removed or disabled in production code to avoid performance overhead and unnecessary logging.


Advanced Tracer

You can create a parameterized version of the @tracer decorator that optionally includes call stack information. Here’s how you can implement it:

# advanced_tracer_example.py
import traceback
import functools
from typing import Optional

def tracer(show_stack: Optional[bool] = False):
    """
    A parameterized decorator that traces function calls and optionally shows the call stack.

    Args:
        show_stack (bool, optional): If True, will include the call stack in the trace output. Defaults to False.
    """
    def decorator(func):
        @functools.wraps(func)  # Preserves the original function's metadata
        def wrapper(*args, **kwargs):
            # Print function entry
            print(f"\nEntering function: {func.__name__}")
            print(f"Arguments: args={args}, kwargs={kwargs}")

            # Show call stack if requested
            if show_stack:
                print("\nCall Stack:")
                # Get the call stack excluding this wrapper function
                stack = traceback.extract_stack()[:-1]  # Remove last entry (this wrapper)
                for filename, lineno, name, line in stack:
                    print(f"  File {filename}, line {lineno}, in {name}")
                    if line:
                        print(f"    {line.strip()}")

            # Execute the function
            try:
                result = func(*args, **kwargs)

                # Print function exit
                print(f"\nExiting function: {func.__name__}")
                print(f"Return value: {result}")

                return result
            except Exception as e:
                print(f"\nException in function {func.__name__}:")
                if show_stack:
                    print("\nException Stack Trace:")
                    traceback.print_exc()
                raise  # Re-raise the exception

        return wrapper
    return decorator

# Example usage with and without stack trace
@tracer()  # Without stack trace
def simple_function(x: int, y: int):
    return x + y

@tracer(show_stack=True)  # With stack trace
def recursive_function(n: int):
    if n <= 1:
        return 1
    return n * recursive_function(n - 1)

@tracer(show_stack=True)  # Exception, with stack trace
def keyerror_function(d: dict, key: str):
        print(d[key])

# Usage example
if __name__ == "__main__":
    # Test simple function
    print("\nTesting simple function:")
    result = simple_function(5, 3)
    print()
    print("=" * 30)

    # Test recursive function with stack trace
    print("\nTesting recursive function:")
    result = recursive_function(3)
    print()
    print("=" * 30)

    # Test KeyError Exception
    d = {"found": 1}
    keyerror_function(d, "notfound")

Key features of this implementation:

  1. Optional Parameter: The decorator can be used with or without parameters:
@tracer()  # Without stack trace
@tracer(show_stack=True)  # With stack trace
  1. Stack Trace Information: When show_stack=True, it shows:
  • The call stack leading to the function
  • File names and line numbers
  • The actual lines of code being executed
  1. Exception Handling: Captures and displays exceptions with stack traces if enabled

  2. Proper Function Wrapping: Uses @functools.wraps to preserve the original function’s metadata

Example output for the recursive function with stack trace enabled:

Traceback (most recent call last):
  File "/Users/john/h/06/advanced_tracer_example.py", line 32, in wrapper
    result = func(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/john/h/06/advanced_tracer_example.py", line 62, in keyerror_function
    print(d[key])
          ~^^^^^
KeyError: 'notfound'

Testing simple function:

Entering function: simple_function
Arguments: args=(5, 3), kwargs={}

Exiting function: simple_function
Return value: 8

==============================

Testing recursive function:

Entering function: recursive_function
Arguments: args=(3,), kwargs={}

Call Stack:
  File /Users/john/h/06/advanced_tracer_example.py, line 74, in <module>
    result = recursive_function(3)

Entering function: recursive_function
Arguments: args=(2,), kwargs={}

Call Stack:
  File /Users/john/h/06/advanced_tracer_example.py, line 74, in <module>
    result = recursive_function(3)
  File /Users/john/h/06/advanced_tracer_example.py, line 32, in wrapper
    result = func(*args, **kwargs)
  File /Users/john/h/06/advanced_tracer_example.py, line 58, in recursive_function
    return n * recursive_function(n - 1)

Entering function: recursive_function
Arguments: args=(1,), kwargs={}

Call Stack:
  File /Users/john/h/06/advanced_tracer_example.py, line 74, in <module>
    result = recursive_function(3)
  File /Users/john/h/06/advanced_tracer_example.py, line 32, in wrapper
    result = func(*args, **kwargs)
  File /Users/john/h/06/advanced_tracer_example.py, line 58, in recursive_function
    return n * recursive_function(n - 1)
  File /Users/john/h/06/advanced_tracer_example.py, line 32, in wrapper
    result = func(*args, **kwargs)
  File /Users/john/h/06/advanced_tracer_example.py, line 58, in recursive_function
    return n * recursive_function(n - 1)

Exiting function: recursive_function
Return value: 1

Exiting function: recursive_function
Return value: 2

Exiting function: recursive_function
Return value: 6

==============================

Entering function: keyerror_function
Arguments: args=({'found': 1}, 'notfound'), kwargs={}

Call Stack:
  File /Users/john/h/06/advanced_tracer_example.py, line 80, in <module>
    keyerror_function(d, "notfound")

Exception in function keyerror_function:

Exception Stack Trace:
Traceback (most recent call last):
  File "/Users/john/h/06/advanced_tracer_example.py", line 80, in <module>
    keyerror_function(d, "notfound")
  File "/Users/john/h/06/advanced_tracer_example.py", line 32, in wrapper
    result = func(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/john/h/06/advanced_tracer_example.py", line 62, in keyerror_function
    print(d[key])
          ~^^^^^
KeyError: 'notfound'

This implementation is particularly useful for:

  1. Debugging complex call hierarchies
  2. Understanding recursion paths
  3. Tracking down issues in large applications
  4. Performance profiling when combined with timing information

You can also extend this further by adding more parameters for different levels of detail or specific types of information you want to track.