from IPython.display import display,Markdown
import textwraptracetools
tracetool is an LLM tool that traces function execution and captures variable snapshots after each line runs, using Python 3.12’s sys.monitoring for low-overhead tracing.
For each call to the target function (including recursive calls), the LLM receives a stack trace showing how that call was reached, plus a per-line trace dict mapping each executed source line to its hit count and variable snapshots. Variables that don’t change are shown as a single (type, repr) tuple, while those that evolve across iterations appear as a list of snapshots.
This is particularly useful when the LLM needs to understand unfamiliar code by seeing exactly what happens step-by-step, debug issues by watching how variables change, verify loop behavior by confirming iteration counts and accumulator values, or explore recursive functions where each call gets its own trace entry.
tracetool
def tracetool(
sym:str, # Dotted symbol path of callable to run
args:list=None, # Positional args for callable (JSON values passed directly)
kwargs:dict=None, # Keyword args for callable (JSON values passed directly)
target_func:str=None, # Dotted symbol path of function to trace (defaults to sym)
)->list: # List of (stack_str,trace_dict); trace_dict maps source snippets to (hit_count, variables), unchanged vars collapsed to single tuple
Trace execution using sys.monitoring (Python 3.12+), returning a list of per-call traces.
Return: - list of length <= 10 - one element per call to target_func (including recursion) - each element is: (stack_str, trace_dict) stack_str: call stack string (filtered so fn is the shallowest frame shown) trace_dict: { “
Semantics: - “Line” means an AST-level line: separate statements (even if on one physical line via ;). - Compound statements are keyed by their header only. - Unchanged variables → ('type', 'repr') tuple, changed variables → [('type', 'repr'), ...] list. - Comprehensions are treated as a line node and are monitored, including inside the comprehension frame, with per-iteration snapshots. - Snapshots are recorded after each line finishes, so assignments show updated values.
def demo(n,m='x'):
total = 0
for i in range(n): total += i
return m*totalTry using &tracetool to trace demo(5, m='y'). Based on the results, tell me how demo works.
🤖Reply🤖
🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠🧠
tracetool(sym=demo, args=[5], kwargs={‘m’: ‘y’})
{
"id": "toolu_01CeHDU3L8nuEZZL6xCnRpE4",
"call": {
"function": "tracetool",
"arguments": {
"sym": "demo",
"args": "[5]",
"kwargs": "{'m': 'y'}"
}
},
"result": "[('',\n {'total = 0': (1, {'total': ('int', '0')}),\n 'for i in range(n):': (6,\n {'i': [('int', '0'),\n ('int', '1'),\n ('int', '2'),\n ('int', '3'),\n ('int', '4'),\n ('int', '4')],\n 'n': ('int', '5')}),\n 'total += i': (5,\n {'i': [('int', '0'),\n ('int', '1'),\n ('int', '2'),\n ('int', '3'),\n ('int', '4')],\n 'total': [('int', '0'),\n ('int', '1'),\n ('int', '3'),\n ('int', '6'),\n ('int', '10')]}),\n 'return m*total': (1, {'m': ('str', \"'y'\"), 'total': ('int', '10')})})]"
}The demo function:
- Initializes
total = 0 - Loops through
range(n)wheren=5, iterating withifrom 0 to 4 - Accumulates the sum by adding each
itototal(0→0, +1→1, +3→3, +6→6, +10→10) - Returns
m * total, which multiplies the string'y'by 10, giving'yyyyyyyyyy'
So demo(n, m) computes the sum of integers from 0 to n-1 (which is n*(n-1)/2), then repeats the string m that many times. For demo(5, m='y'), it returns 'y' * 10 = 'yyyyyyyyyy'.
def quotefunc(s): return textwrap.wrap('aaa '*10, width=10, subsequent_indent='> ')
tests = 'aaa '*10
quotefunc(tests)['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa']
Try tracing textwrap.TextWrapper._wrap_chunks by calling quotefunc.
fmt_trace
def fmt_trace(
traces
):
Format tracetool output as markdown
For a human friendly version, use fmt_trace on the result.
r = tracetool(sym='quotefunc', args=['test'], target_func='textwrap.TextWrapper._wrap_chunks')Markdown(fmt_trace(r))Stack:
quotefunc (1868596414.py:1)
wrap ($PYTHON_STDLIB/textwrap.py:384)
TextWrapper.wrap ($PYTHON_STDLIB/textwrap.py:359)
TextWrapper._wrap_chunks ($PYTHON_STDLIB/textwrap.py:238)
| Source | Hits | Variables |
|---|---|---|
lines = [] |
1 | lines: [] |
if self.width <= 0: |
1 | self: <textwrap.TextWrapper object> |
if self.max_lines is not None: |
1 | self: <textwrap.TextWrapper object> |
chunks.reverse() |
1 | chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... |
while chunks: |
6 | chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... → [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... → [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... → [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'] → [' ', 'aaa', ' ', 'aaa'] → [] |
cur_line = [] |
5 | cur_line: [] |
cur_len = 0 |
5 | cur_len: 0 |
if lines: |
5 | lines: [] → ['aaa aaa'] → ['aaa aaa', '> aaa aaa'] → ['aaa aaa', '> aaa aaa', '> aaa aaa'] → ['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa'] |
indent = self.subsequent_indent |
4 | indent: '> 'self: <textwrap.TextWrapper object> |
indent = self.initial_indent |
1 | indent: ''self: <textwrap.TextWrapper object> |
width = self.width - len(indent) |
5 | indent: '' → '> ' → '> ' → '> ' → '> 'self: <textwrap.TextWrapper object>width: 10 → 8 → 8 → 8 → 8 |
if self.drop_whitespace and chunks[-1].strip() == '' and lin... |
5 | chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... → [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... → [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... → [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'] → [' ', 'aaa', ' ', 'aaa']lines: [] → ['aaa aaa'] → ['aaa aaa', '> aaa aaa'] → ['aaa aaa', '> aaa aaa', '> aaa aaa'] → ['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa']self: <textwrap.TextWrapper object> |
while chunks: |
25 | chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... |
l = len(chunks[-1]) |
24 | chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...l: 3 → 1 → 3 → 1 → 3 → 3 → 1 → 3 → 1 → 3 |
if cur_len + l <= width: |
24 | cur_len: 0 → 3 → 4 → 7 → 8 → 0 → 3 → 4 → 7 → 8l: 3 → 1 → 3 → 1 → 3 → 3 → 1 → 3 → 1 → 3width: 10 → 10 → 10 → 10 → 10 → 8 → 8 → 8 → 8 → 8 |
cur_line.append(chunks.pop()) |
20 | chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'...cur_line: ['aaa'] → ['aaa', ' '] → ['aaa', ' ', 'aaa'] → ['aaa', ' ', 'aaa', ' '] → ['aaa'] → ['aaa', ' '] → ['aaa', ' ', 'aaa'] → ['aaa', ' ', 'aaa', ' '] → ['aaa'] → ['aaa', ' '] |
cur_len += l |
20 | cur_len: 3 → 4 → 7 → 8 → 3 → 4 → 7 → 8 → 3 → 4l: 3 → 1 → 3 → 1 → 3 → 1 → 3 → 1 → 3 → 1 |
break |
4 | |
if chunks and len(chunks[-1]) > width: |
5 | chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... → [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... → [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'] → [' ', 'aaa', ' ', 'aaa'] → []width: 10 → 8 → 8 → 8 → 8 |
if self.drop_whitespace and cur_line and cur_line[-1].strip(... |
5 | cur_line: ['aaa', ' ', 'aaa', ' ']self: <textwrap.TextWrapper object> |
cur_len -= len(cur_line[-1]) |
5 | cur_len: 7cur_line: ['aaa', ' ', 'aaa', ' '] |
del cur_line[-1] |
5 | cur_line: ['aaa', ' ', 'aaa'] |
if cur_line: |
5 | cur_line: ['aaa', ' ', 'aaa'] |
if (self.max_lines is None or |
5 | chunks: [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... → [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'... → [' ', 'aaa', ' ', 'aaa', ' ', 'aaa', ' ', 'aaa'] → [' ', 'aaa', ' ', 'aaa'] → []cur_len: 7lines: [] → ['aaa aaa'] → ['aaa aaa', '> aaa aaa'] → ['aaa aaa', '> aaa aaa', '> aaa aaa'] → ['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa']self: <textwrap.TextWrapper object>width: 10 → 8 → 8 → 8 → 8 |
lines.append(indent + ''.join(cur_line)) |
5 | cur_line: ['aaa', ' ', 'aaa']indent: '' → '> ' → '> ' → '> ' → '> 'lines: ['aaa aaa'] → ['aaa aaa', '> aaa aaa'] → ['aaa aaa', '> aaa aaa', '> aaa aaa'] → ['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aaa'] → ['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aa... |
return lines |
1 | lines: ['aaa aaa', '> aaa aaa', '> aaa aaa', '> aaa aa... |