tracefunc

A lightweight Python function tracer that captures per-statement execution counts and variable values.

tracefunc takes a function and its arguments, executes it, and returns a list of per-call traces. Each entry is (stack_str, trace_dict) where stack_str is the call stack (filtered so fn is the shallowest frame shown; empty when target_func is None) and trace_dict maps AST-level snippets to (hit_count, vars_map) with per-hit samples. Comprehensions show up as their own lines with per-iteration values.

Install

pip install tracefunc

Requires Python 3.12+ (uses sys.monitoring instruction events).

How to use

from tracefunc import tracefunc
from pprint import pprint

Simple function

Here’s a simple example tracing a loop:

def demo(n):
    total = 0
    for i in range(n): total += i
    return total
def show_res(x):
    for snippet, (hits, vars_map) in x.items():
        print('-', repr(snippet), hits)
        pprint(vars_map)
stack, result = tracefunc(demo, 3, target_func=demo)[0]
print(stack)
show_res(result)
demo (2643322203.py:1)
- 'total = 0' 1
{'total': [('int', '0')]}
- 'for i in range(n):' 4
{'i': [('int', '0'), ('int', '1'), ('int', '2'), ('int', '2')],
 'n': [('int', '3'), ('int', '3'), ('int', '3'), ('int', '3')],
 'range': [('type', "<class 'range'>"),
           ('type', "<class 'range'>"),
           ('type', "<class 'range'>"),
           ('type', "<class 'range'>")]}
- 'total += i' 3
{'i': [('int', '0'), ('int', '1'), ('int', '2')],
 'total': [('int', '0'), ('int', '1'), ('int', '3')]}
- 'return total' 1
{'total': [('int', '3')]}

Multiple statements on one physical line

Semicolon-separated statements are tracked separately.

def one_liner(): x = 1; y = 2; return x + y

_, res = tracefunc(one_liner)[0]
show_res(res)
- 'x = 1' 1
{'x': [('int', '1')]}
- 'y = 2' 1
{'y': [('int', '2')]}
- 'return x + y' 1
{'x': [('int', '1')], 'y': [('int', '2')]}

Targeted tracing and call stacks

You can trace a specific target function and see the call stack for each call. Stack paths are shown relative to fn’s directory when possible.

def target(x):
    return x + 1

def another(x): return target(x)

def wrapper(n):
    out = []
    for i in range(n): out.append(target(i))
    out.append(another(10))
    return out

for stack, res in tracefunc(wrapper, 2, target_func=target):
    print(stack)
    show_res(res)
wrapper (1041865549.py:8)
target (1041865549.py:1)
- 'return x + 1' 1
{'x': [('int', '0')]}
wrapper (1041865549.py:8)
target (1041865549.py:1)
- 'return x + 1' 1
{'x': [('int', '1')]}
wrapper (1041865549.py:9)
another (1041865549.py:4)
target (1041865549.py:1)
- 'return x + 1' 1
{'x': [('int', '10')]}

Nested function

Nested definitions appear as statements, and their bodies are traced when called.

def outer(x):
    def inner(y):
        return x + y
    return inner(5)

_, res = tracefunc(outer, 10)[0]
show_res(res)
- 'def inner(y):' 1
{'inner': [('function', '<function outer.<locals>.inner>')]}
- 'return x + y' 1
{'x': [('int', '10')], 'y': [('int', '5')]}
- 'return inner(5)' 1
{'inner': [('function', '<function outer.<locals>.inner>')]}