Debug

Xonsh supports two complementary ways to debug xonsh code:

  • Debug in IDE — attach a full graphical debugger from an editor like VS Code.

  • Instant debugging — drop into a debugger at the exact call site with @.debug, with automatic engine selection and a xonsh-syntax REPL when no external debugger is installed.

Debug in IDE

Xonsh extension for VS Code provides syntax highlighting and basic language support for .xsh files. Install via the extensions menu.

Instant debugging

When you need to stop execution at a specific call site without wiring up an IDE, xonsh ships a debugging helper attached to every session as @.debug. It works like Python’s builtin breakpoint(), but with automatic engine selection, session-aware fallbacks, and a xonsh-syntax REPL when no external debugger is installed.

Quick Start

Drop into a debugger at any point in your xonsh code:

@.debug.breakpoint()

The engine is chosen automatically in priority order: pdbpipdbpdbexecereval. The first one that is importable (or, for execer, available on the session) wins.

To force a specific engine for a single call:

@.debug.breakpoint(engine='pdbp')
@.debug.breakpoint(engine='ipdb')
@.debug.breakpoint(engine='pdb')
@.debug.breakpoint(engine='execer')   # xonsh REPL at the call site
@.debug.breakpoint(engine='eval')     # plain-Python REPL

Setting the Default Engine

Set $XONSH_DEBUG_BREAKPOINT_ENGINE in your xonsh RC to change the default used when engine is not passed (or is 'auto'):

$XONSH_DEBUG_BREAKPOINT_ENGINE = 'pdbp'

Allowed values: 'auto' (default), 'pdbp', 'ipdb', 'pdb', 'execer', 'eval'.

An explicit argument to breakpoint() always beats the env var.

Wrapping breakpoint() in a helper

If you build your own dev-helper that calls @.debug.breakpoint() for you, the debugger would normally stop inside the helper, which is rarely what you want. Pass an explicit frame= to relocate the stop site to any frame on the stack — typically the helper’s caller. This mirrors pdbp.set_trace(frame=...).

@ def my_dev_helper():
     print("dropping in…")
     @.debug.breakpoint(frame=@.imp.sys._getframe().f_back)
@ my_dev_helper()
dropping in…
BREAKPOINT WITH 'pdbp'
# …debugger now sits at my_dev_helper's caller, not inside the helper.

When frame= is omitted (the default), the debugger uses the immediate caller’s frame, exactly as before.

Engines

Engine

Description

pdbp

pdbp — enhanced pdb (sticky mode, syntax highlighting, where/u/d frame hiding). Install with xpip install pdbp.

ipdb

ipdb - IPython-flavored pdb. Install with xpip install ipdb.

pdb

Stdlib pdb. Always available.

execer

A REPL at the caller’s frame backed by the session’s Execer. Full xonsh syntax is available — subprocesses (ls, $(ls)), env lookups (@.env['HOME']), aliases, and @. attribute access. Raises RuntimeError if no execer is attached to the session.

eval

A minimal REPL using plain Python eval/exec. Has no dependency on a xonsh session — works in detached contexts (scripts, tests) the same as inside an interactive shell.

When engine='auto' resolves to an engine, @.debug prints a short banner identifying the choice and a one-line hint about how to continue or abort, then drops into that engine.

Tab completion in callable aliases

xonsh runs callable aliases (registered via @aliases.register) in worker threads. CPython only wires readline into the main thread, so the pdbp, ipdb, and pdb engines lose tab completion when invoked from inside an alias — the TAB key inserts a literal tab into the underlying input() call instead of triggering completion.

When engine='auto' is resolved from a non-main thread, @.debug auto-walks past the readline-based engines and selects execer (or eval if no session is attached). Both REPLs use input() directly with no completion, so the missing TAB matches the prompt and there is no false expectation.

If you set $XONSH_DEBUG_BREAKPOINT_ENGINE or pass engine= to one of the readline-based engines, @.debug still honours your choice from a worker thread but prints a UserWarning so the broken TAB is not surprising. The warning is informational — pdbp/ipdb/pdb still work, just without tab completion.

This is a CPython limitation, not a xonsh bug.

REPL Commands (execer and eval engines)

Both execer and eval engines start a small REPL at the caller’s frame. The REPL accepts any Python/xonsh expression or statement, and recognizes the following control commands:

Command

Effect

c / cont / continue

Resume execution after the breakpoint.

exit / quit / q

Abort execution — raises xonsh.debug.XonshDebugQuit, which propagates out of the breakpoint() call and unwinds the stack.

EOF / Ctrl-C

Same as continue (least destructive default).

Expression results are printed automatically. Statements (assignments, loops, etc.) run in the caller’s frame, so local variables are visible and modifiable:

execer> @.env['HOME']
'/Users/you'
execer> ls *.py | head -3
debug.py
environ.py
tools.py
execer> my_local_var = 42
execer> c

Routing Python’s builtin breakpoint() Through @.debug

PEP 553 makes Python’s builtin breakpoint() go through sys.breakpointhook. @.debug can install a hook that routes every builtin breakpoint() call through the same engine as @.debug.breakpoint(). Add the following to your xonsh RC:

$XONSH_DEBUG_BREAKPOINT_ENGINE = 'pdbp'
@.debug.replace_builtin_breakpoint()

After this, breakpoint() anywhere in xonsh code — or in any plain Python module loaded inside the session — drops into the configured engine at the call site. To restore Python’s default behavior:

import sys
sys.breakpointhook = sys.__breakpointhook__