Skip to main content

judb

CI

A browser-based visual Python debugger for datascience - debug any Python module or function, with the full power of jupyter notebooks.

judb uses pudb-style stepping with a notebook-style rich console that executes cells in the currently-paused stack frame. Plot and inspect a paused frame's real objects — DataFrames, arrays, xarray Datasets, matplotlib figures — the way you would in a notebook.

judb's four-pane browser UI: Source, notebook console, Variables, and Call stack, paused in a frame

Status

Early prototype.

Try it out

Requires Python ≥ 3.13.

uv add --dev judb
# Or if you use pip
pip install judb
# or, in a checkout of this repo
uv sync

Drop judb.set_trace() where you want to pause:

import judb

def analyze(df):
    judb.set_trace()      # opens a browser tab, paused on the next line
    return df.describe()

Run your script normally. A browser tab opens at the paused frame with four panes — Source, a notebook console, Variables, and the Call stack. Step with the toolbar (Continue / Next / Step / Return); in the console, type Python that runs in the paused frame:

df                        # rich HTML table
df["x"].rolling(5).mean() # any expression, evaluated in-frame
import matplotlib.pyplot as plt; plt.plot(df["x"])   # inline figure

The console is a real notebook: cells are editable and re-runnable, and can be added, deleted, and reordered. Values you keep coming back to go in the Watch pane — pin an expression like df.shape or arr.mean() and it is re-evaluated every time you stop, change frame, or run a cell. Open file… in the Source pane shows any file of your project, so you can set a breakpoint in code the run hasn't reached yet and continue straight to it. You can also wire judb up as the breakpoint() hook, no code change needed:

PYTHONBREAKPOINT=judb.set_trace python your_script.py

Or run a whole script or module under judb without touching it (stops at the first line):

python -m judb your_script.py [args...]
python -m judb -m your.pipeline.module [args...]

Debugging a failing test

Point pytest's post-mortem debugger at judb, and a failing test drops you into the browser UI paused at the failure, with the console live in that frame:

pytest --pdb --pdbcls=judb:Debugger

Inspect the assertion's operands, plot the offending array, poke at locals — all in the frame where the test blew up. Hit Continue to move on.

With --pdbcls=judb:Debugger set, pytest's other entry points reach judb too — --trace breaks at the first line of every test, and a breakpoint() inside a test opens the UI there:

pytest --trace --pdbcls=judb:Debugger      # break at the start of each test
pytest --pdbcls=judb:Debugger              # honour breakpoint() in a test

Prefer a ready-made demo? From a checkout:

uv run python scripts/demo_p2.py     # a small paused frame with an array to plot

uv sync --extra example              # heavier viz libs (xarray/plotly/bokeh/altair/polars)
uv run python scripts/demo_rich.py   # a spread of rich objects to inspect

Interactive plots (zoom & pan)

By default a plot renders as a static inline PNG. For interactive figures — zoom, pan, the full matplotlib toolbar, live while paused — run this once in the console, then plot as usual:

%matplotlib judb                     # judb's interactive backend
import matplotlib.pyplot as plt
plt.plot(signal)                     # a live, zoomable/pannable canvas

Switch back to static images at any time with %matplotlib inline. Standard IPython magics work too (%timeit, %who, %%time, %matplotlib inline, …). To start every session that way, set figure_format = "interactive" (see Configuration).

Under the hood this is matplotlib's own WebAgg engine (the same one behind %matplotlib notebook) driven over judb's connection — no Jupyter kernel required. Interactivity is live whenever the debuggee is paused and freezes on Continue, since the figure lives in the paused frame.

Configuration

judb runs with no configuration at all. When you do want to change how it starts, put the settings in your project's pyproject.toml:

[tool.judb]
open_browser = true          # open a browser tab on start (the URL is always printed)
stop_on_entry = true         # `python -m judb`: pause on the target's first line
break_on_exception = true    # `python -m judb`: land in post-mortem on an uncaught crash
figure_format = "png"        # "png" for inline images, "interactive" for live figures

stop_on_entry = false is the "start it and walk away" mode: the program runs at full speed and judb only takes over when something goes wrong (post-mortem on the crash) or when your code asks it to (breakpoint()). Note that a breakpoint you set in the gutter needs the program to still be traced, so plan to set them during the entry stop — or put a breakpoint() in the code.

The same keys — without the [tool.judb] header, since it is judb's own file — go in ~/.config/judb/config.toml (or $XDG_CONFIG_HOME/judb/config.toml) to apply to every project. A project's settings win over your personal ones, key by key, and python -m judb's own flags win over both for a single run:

python -m judb --no-stop-on-entry --no-browser train.py --epochs 3

judb's flags go before the script; everything after it belongs to the script, so --epochs 3 above reaches train.py. Run python -m judb --help for the full list. A setting judb does not recognise is a warning on stderr, not an error — a stale key never stops you from debugging.

Two things live in the browser instead, because that is where they are used: the light/dark theme (the toolbar toggle) and your watch expressions, both remembered per browser. And JUDB_NO_BROWSER=1 in the environment suppresses the browser tab whatever the configuration says, for headless boxes and CI.

Threads and processes

judb debugs one paused frame at a time. Where that frame lives changes what works:

Where set_trace() runs Status
The main thread (a normal script, python -m judb, pytest) Fully supported
A single worker thread Supported, with two caveats below
Two threads pausing at the same time Not supported — see below
A child process (multiprocessing, fork, spawn) Supported — each process gets its own UI

A single worker thread pauses, shows its frame, and runs console cells in it exactly as the main thread does. Two things degrade, both because only the main thread can receive signals:

  • Interrupt falls back from a real SIGINT to Python's async-exception API, which lands at the next bytecode. It still stops a Python loop, but it cannot break out of a blocking call like time.sleep until that call returns.
  • Ctrl+C in the terminal will not end the program. The KeyboardInterrupt is delivered to the main thread, while the paused worker keeps waiting; if it is a non-daemon thread the process will not exit. Use the UI's Quit, or kill the process.

Two threads pausing concurrently is not supported yet (real multi-thread debugging is planned for a later phase). It does not crash, but: only the most recent pause is visible, the other paused thread is invisible while still blocked, and each Continue releases an arbitrary one of them. If your debuggee is multi-threaded, set a breakpoint that only one thread can reach.

Child processes each start their own server on their own port and print their own URL, so you get one browser tab per paused process. This holds for fork too: a forked child does not reuse its parent's server, since the threads running it do not survive the fork.

License

Copyright 2026 Mika Pflüger. Licensed under the Apache License 2.0 (see also NOTICE).

judb's debugger architecture and design borrow heavily from PuDB (MIT/X Consortium license). PuDB's license and attribution notice are reproduced in licenses/pudb-LICENSE.txt. Rich-output CSS is partly derived from Project Jupyter (BSD-3-Clause; see licenses/jupyter-LICENSE.txt). The interactive plotting backend reuses Matplotlib's WebAgg engine and serves its client JS and toolbar assets (BSD-compatible license; see licenses/matplotlib-LICENSE.txt).

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

judb-0.3.0.tar.gz (776.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

judb-0.3.0-py3-none-any.whl (246.3 kB view details)

Uploaded Python 3

File details

Details for the file judb-0.3.0.tar.gz.

File metadata

  • Download URL: judb-0.3.0.tar.gz
  • Upload date:
  • Size: 776.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for judb-0.3.0.tar.gz
Algorithm Hash digest
SHA256 bb06defe7529964c7bf3bf54a9bee00c3e764f89e412fc069e7c7695db816aec
MD5 4ce828c9022baeec77c208b3dfa200be
BLAKE2b-256 5611fa8808bf8d6607dd65c7519cb0c7cd71d8509008dacfdbe6087b07f8b180

See more details on using hashes here.

Provenance

The following attestation bundles were made for judb-0.3.0.tar.gz:

Publisher: release.yml on mikapfl/judb

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file judb-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: judb-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 246.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for judb-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7dc19f07faaad33f6bc38a1e8b1f7d17bb09a5db2e9107ca0082ce61fd0269ad
MD5 4378a1f8343449829250bf40d20de84c
BLAKE2b-256 6bed6deea74179ee57126cf1ad71c4586d09fee056f636469ed5b4207dffa4d9

See more details on using hashes here.

Provenance

The following attestation bundles were made for judb-0.3.0-py3-none-any.whl:

Publisher: release.yml on mikapfl/judb

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page