Overview
pybreakz is a zero-dependency Python debugger CLI built for coding agents. Set breakpoints on specific line numbers, run your script, and get a clean structured snapshot of local variables and call stack state — all in one shot. No interactive session required.
It's particularly useful for AI coding agents like Claude Code. Instead of adding print() statements or reading large log files (which eat up context window), the agent runs pybreakz and gets a precise, structured report at any line in the code. The output is deliberately compact and token-optimised — only the data you asked for, no noise.
pybreakz run script.py --breakpoints 34,67 --watch "x,response", read the report, patch the bug. Repeat if needed. The --on-exception flag is a good first pass — let the crash tell you where to look.
Installation
Install from PyPI:
pip install pybreakz
Requires Python 3.9 or later. Zero pip dependencies — only the standard library.
Or install from source:
git clone https://github.com/allannapier/pybreaks.git
cd pybreaks
pip install -e .
Verify it's working:
pybreakz --version
Quick Start
Point pybreakz at your script and tell it which line numbers to break on:
pybreakz run script.py --breakpoints 7 --watch "item,len(items)"
pybreakz runs the script and produces a structured report for each hit — compact enough to be token-efficient, detailed enough to debug without guessing:
════════════════════════════════════════════════════════════
pybreakz report → script.py
════════════════════════════════════════════════════════════
┌─ BREAKPOINT HIT #1 script.py:7
│ Source: result = process(item)
│
│ Call Stack (innermost last):
│ <module>() [script.py:13]
│ main() [script.py:11]
│ run_batch() [script.py:7]
│
│ Locals:
│ items = list[3 items]: [{'id': 1, 'name': 'foo', 'active': True}, ...]
│ item = dict[3 keys]: {'id': 1, 'name': 'foo', 'active': True}
│ result ! name 'result' is not defined
│ Watched:
│ item = dict[3 keys]: {'id': 1, 'name': 'foo', 'active': True}
│ len(items) = 3
└────────────────────────────────────────────────────────────
┌─ BREAKPOINT HIT #2 script.py:7
│ Source: result = process(item)
│
│ Locals:
│ items = list[3 items]: [...]
│ item = dict[3 keys]: {'id': 2, 'name': 'bar', 'active': False}
│ result = 2
│ Watched:
│ item = dict[3 keys]: {'id': 2, 'name': 'bar', 'active': False}
│ len(items) = 3
└────────────────────────────────────────────────────────────
Basic Breakpoints
Use --breakpoints (or -b) with one or more comma-separated line numbers. pybreakz captures all local variables at each hit.
# Single breakpoint
pybreakz run script.py --breakpoints 42
# Multiple breakpoints
pybreakz run script.py --breakpoints 10,25,42
def/class header lines. Use the first line inside the function body instead.
Watch Expressions
Use --watch (or -w) to evaluate specific expressions in the local scope at each breakpoint hit. More targeted than capturing all locals.
pybreakz run script.py --breakpoints 42 --watch "user_id,response.status,len(items)"
Expressions are comma-separated and evaluated in the local scope at the breakpoint. You can access any variable, call methods, use indexing, or write any valid Python expression.
Eval Expressions
Use --eval (or -e) to evaluate expressions that appear in a separate "Evaluated" section of the output. Useful for computed values you want to distinguish from watched variables.
pybreakz run script.py --breakpoints 42 --eval "df.shape,type(result),items[:3]"
You can use --watch and --eval together in the same command.
On Exception
Use --on-exception (or -x) to capture program state at the point of an unhandled exception — locals, call stack, and full traceback.
# Basic exception capture
pybreakz run script.py --on-exception
# With watched expressions at the crash site
pybreakz run script.py --on-exception --watch "self,request,data"
--on-exception, let the crash tell you where to look, then add targeted --breakpoints on subsequent runs.
Cross-file Breakpoints
Set breakpoints in imported modules, not just the entry script. Use the file:line format instead of just a line number.
# Break in an imported module
pybreakz run script.py --breakpoints "utils.py:55"
# Mix file:line and plain line numbers
pybreakz run script.py --breakpoints "utils.py:55,helpers.py:12,42"
# With a condition
pybreakz run script.py --breakpoints "utils.py:55:x>0,helpers.py:12:done"
Use just the filename (not the full path). pybreakz matches against the end of the module's file path.
Variable Watchpoints
Use --trace-vars (or -V) to record a hit every time a named variable changes value — anywhere in any file. Unlike breakpoints, you don't need to know which line the variable is on.
# Track every change to 'counter' and 'result'
pybreakz run script.py --trace-vars "counter,result"
# Combine with breakpoints
pybreakz run script.py --trace-vars "x" --breakpoints 10
# Track object attributes
pybreakz run script.py --trace-vars "self.state"
Useful when you know a variable is getting an unexpected value but don't know where it's being set. Each hit shows the old and new value:
┌─ WATCHPOINT #1 result changed [script.py:2]
│ Source: result = item['id'] * 2
│ result: 2 → 4
│
│ Call Stack (innermost last):
│ main() [script.py:11]
│ run_batch() [script.py:7]
│ process() [script.py:2]
└────────────────────────────────────────────────────────────
┌─ WATCHPOINT #2 result changed [script.py:7]
│ Source: result = process(item)
│ result: 2 → 4
│
│ Call Stack (innermost last):
│ main() [script.py:11]
│ run_batch() [script.py:6]
└────────────────────────────────────────────────────────────
Live Mode & Servers
Use --live (or -L) to stream breakpoint hits to stderr in real-time as they happen. This is designed for long-running processes like servers — pybreakz won't wait for the script to finish before showing output.
# Stream hits from a long-running server
pybreakz run server.py --breakpoints "handler.py:55" --live --max-hits 0
# Add a delay between hits (useful for agents reading output)
pybreakz run server.py --breakpoints "handler.py:55" --live --delay 2
# Pause at each hit with an interactive REPL (implies --live)
pybreakz run server.py --breakpoints "handler.py:55:len(results)>0" --break
--live --delay 2, wait for startup, trigger the route, then tail the output. The delay gives the agent time to read each hit before execution continues.
Flags
--live/-L— stream hits in real-time. Default timeout becomes 0 (no limit).--break/-B— pause at each hit with an interactive REPL. Implies--live.--delay SECS/-D— pause N seconds after each hit in live mode before continuing execution.
Conditional Breakpoints
Only fire a breakpoint when a condition is true. Append :condition after the line number.
pybreakz run script.py --breakpoints "42:x>100,67:status=='error'"
The condition is evaluated in the local scope at that line. If it evaluates to falsy, the breakpoint is skipped silently. Useful when a breakpoint is inside a loop and you only care about specific iterations.
Passing Arguments to Your Script
Use -- to separate pybreakz flags from your script's own arguments. Everything after -- is forwarded as sys.argv.
pybreakz run script.py --breakpoints 10 -- --input data.csv --verbose
JSON Output
Use --format json to get machine-readable output. Includes all breakpoint hits, locals, watched values, and call stack data.
pybreakz run script.py --breakpoints 42 --format json
Useful if you want to pipe the output into another tool or process it programmatically.
Timeout & Limits
Control how long pybreakz lets a script run and how many breakpoint hits to record.
# 60 second timeout
pybreakz run script.py --breakpoints 10 --timeout 60
# No timeout
pybreakz run script.py --breakpoints 10 --timeout 0
# Limit breakpoint hits recorded
pybreakz run script.py --breakpoints 10 --max-hits 5
Default timeout is 30 seconds. Default max hits is 20. Set --max-hits 0 for unlimited hits.
Options Reference
| Flag | Short | Default | Description |
|---|---|---|---|
| --breakpoints LINES | -b |
— | Breakpoints to set. Formats: LINE, LINE:CONDITION, FILE:LINE, FILE:LINE:CONDITION. Comma-separated. |
| --trace-vars NAMES | -V |
— | Comma-separated variable names. Records a hit whenever any of these change value, in any file. |
| --watch EXPRS | -w |
— | Comma-separated expressions evaluated at each hit |
| --eval EXPRS | -e |
— | Additional expressions shown in a separate output section |
| --on-exception | -x |
off | Capture locals, stack, and traceback on unhandled exception |
| --live | -L |
off | Stream breakpoint hits to stderr in real-time. Designed for servers and long-running processes. Default timeout becomes 0. |
| --break | -B |
off | Pause at each hit with an interactive REPL. Implies --live. |
| --delay SECS | -D |
— | Pause N seconds after each hit in live mode before execution continues. |
| --timeout SECS | -t |
30 | Script timeout in seconds. Set to 0 for no limit. Defaults to 0 in live/break mode. |
| --format FORMAT | -f |
text | Output format: text or json |
| --max-hits N | — | 20 | Max total hits to record across breakpoints and watchpoints. Set to 0 for unlimited. |
Limitations
- Works on
.pyscripts run directly. For long-running servers use live mode. - Breakpoints must be on executable lines — not blank lines, comments, or
def/classheaders. Use the first line inside the function body. sys.settracehas a small performance overhead — not suitable for tight performance benchmarks.- Multi-threaded scripts: only the main thread is traced — breakpoints in worker threads will not fire.
- Async/await with
asyncioworks for standard patterns. Complex event loop customisation is untested.
For issues or feature requests, open an issue on GitHub.