GitHub - minimaxir/psnek: A modern reimagining of the Python progresssss bar.

GitHub

8 min read Original article ↗

A modern reimagining of the Python progresssss bar.

psnek not only supports the expected features of progress bars such as nesting and writing, but it also supports adding more metrics and logs to easily see how important data changes as the progress bar...progresses.

psnek.mp4

Additionally, the bars are extremely customizable, from changing the gradient color, to changing whether text is printed above or below the progress bar, to changing the height of the progress bar to fit even more metrics!

psnek_tall.mp4

More useful features of psnek:

  • Semantic colorling of Bars that turn amber when a loop stalls and red when it's stuck, adjusting dynamically to the current pace of the progress bar.
  • A statistically significant completion ETA that is marked with a ~ when it can't be trusted with confidence.
  • Support for printing pandas and polars DataFrames (or just a list of dicts) directly into the panel with proper formatting. (fun tip: try sorting a DataFrame before writing to see the top results!)
  • System notification support (OSC 9/99/777) for terminal emulators that support it (e.g. Ghostty, kitty) to alert the user to completion and stalls. These will only fire for long-running tasks to prevent annoyances.
  • Support for CI and AI coding agents by automatically disabling styles and avoiding carriage returns.
  • Zero Python dependencies so importing is fast and psnek is unlikely to break with future Python releases.
  • A dedicated Jupyter Notebook renderer which owns all text output during it and works nicely when rendered in GitHub's Notebook viewer. It also avoids an issue present in tqdm.notebook that massively bloats the resulting notebook with dead widgets.
  • Async/Multithreading support for threads, processes, and asyncio. Relatedly, there is also support for free-threaded Python.
  • Strong performance despite all the features added above: psnek has less than 1/10th the overhead of tqdm and ties/beats tqdm in most other comparisons. (Benchmarks)

The notebooks folder contains many detailed and interactive Jupyter Notebook examples which show how psnek works!

Installation

psnek can be installed from PyPI (Python 3.11+):

Demo

Wrap any iterable with track() and optionally give it a description.

import psnek

total = 0
numbers = range(1, 121)
for number in psnek.track(numbers):
    total += number

print(total)

For anything else, you can update a Bar manually, from any thread. Use with so the bar is always closed and so an Exception marks it as failed:

options = psnek.Options(unit="B", scale="iec")

with psnek.Bar(total=size, desc="upload", options=options) as bar:
    for chunk in chunks:
        send(chunk)
        bar.update(len(chunk))

You can add status fields after the metrics with status(), either as keywords or as a dict whose keys don't need to be valid Python names. Setting a field to None removes it.

bar.status(loss=0.137, lr=3e-4)
bar.status({"val loss": 0.142, "lr": None})

Bars started inside another bar's loop are drawn as a tree below it. By default, only the top-level bar's final line is kept once it finishes.

for epoch in psnek.track(3, "epochs"):
    for batch in psnek.track(batches, f"epoch {epoch + 1}"):
        ...
⣄ epochs    ⣿⣿⣿⣿⣿⣿⣿⣿⡆⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀   33.3%        1/3    0:21<0:22   11.1s/it
└─⠋ epoch 2 ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀   41.2%   412/1000    0:09<0:12   45.8it/s
  └─⢰ eval  ⣿⣿⣿⣿⣿⣿⡏⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣀   26.6%      17/64    0:00<0:01   41.0it/s

To print a line above whatever bars are live:

psnek.write("finished shard", 3)

Jupyter Notebook with a tour of everything above.

The Snek

Each cell of the bar is an 8-dot Braille character that fills in eight steps, so consecutive cells read as one winding coil: ⠀ ⡀ ⡄ ⡆ ⡇ ⡏ ⡟ ⡿ ⣿. When the total is unknown, the snake slithers back and forth, led by a face that reflects the run's mood. If progress stops for an unusually long time for that loop, the snake slows down and the bar turns amber (stalled 12s), then red (stuck 48s).

The Panel

While a bar is live in a terminal, anything you print() or log is routed into a panel drawn below the bars instead of breaking them. You can also write to the panel directly:

bar.log("processed", path)  # append a message to the panel, like logging
bar.log("loss spiked", level=logging.WARNING)  # with a logging level
bar.panel("worker 1: idle\nworker 2: busy")  # replace the panel's content
bar.panel(None)  # clear it
bar.write("saved model")  # print permanently above the bars, like print()

log() is for messages about the run: each one starts with its level (debug, info, warn, error, or fatal), and rises in salience with it, from a faint debug line to bold warnings and errors in the bar's own colors. Options(log_time=True) starts each message with the system time. Logged messages stay indented under the bars once the panel pushes them above, while write() prints its text as-is, flush left:

  16:30:33 info  batch 10 ingested
checkpoint saved: ingest-015.parquet
⢰ ingest ⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣀   90.0%      27/30    0:00<0:00   38.5it/s  rows=27000
⡇ 16:30:33 warn  batch 18: 3 malformed rows skipped
⡇ 16:30:33 error upload of batch 24 failed
⡇                retrying in 1s

panel() also lays out structured data as a borderless table: a dict, dataclass, or named tuple becomes bold keys beside their values, while a list of them (or a polars or pandas DataFrame) becomes rows under a bold header.

bar.panel({"loss": 0.13761, "learning_rate": 3e-4, "epoch": 3})
bar.panel([{"model": "llama", "acc": 0.912}, {"model": "gpt2", "acc": 0.6, "note": "base"}])
⡇ loss           0.1376
⡇ learning_rate  0.0003000
⡇ epoch          3

⡇ model  acc     note
⡇ llama  0.9120
⡇ gpt2   0.6000  base

Jupyter Notebook

CI and AI Agents

Without a terminal (CI, pipes, and AI coding agents such as Claude Code or Codex), psnek prints plain, newline-terminated status lines with no escape codes: a start line, one every 10 seconds, one whenever a bar stalls, gets stuck, or recovers, and a final summary.

ingest: 0.0% 0/100 0:00<--:-- --it/s
ingest: 50.0% 50/100 0:06<~0:06 stalled 5s 40.6it/s
ingest: 53.0% 53/100 0:07<~0:06 7.3it/s (recovered)
ingest: 100.0% 100/100 0:08 done 13.9it/s

psnek detects common CI and agent environment variables automatically. (Jupyter Notebook)

Parallel and Async

pmap() maps a function over items with progress and returns the results in input order.

results = psnek.pmap(fetch, urls, "fetch", workers=16)

For asyncio, atrack() wraps an async iterable, gather() works like asyncio.gather(), and amap() works like pmap() with at most workers calls running at once. Both gather() and amap() count each call as it completes, and cancel the remaining calls if one fails.

async for message in psnek.atrack(stream, "messages"):
    ...

pages = await psnek.gather(*(fetch_page(url) for url in urls), desc="fetch")
pages = await psnek.amap(fetch_page, urls, "fetch", workers=16)

Jupyter Notebook

On a free-threaded build of Python such as python3.14t, CPU-bound pmap() on plain threads scales across cores and update() stays cheap even when many threads call it at once. (Jupyter Notebook)

Options

psnek.Options holds the presentation settings, and can be shared by any number of bars. (Jupyter Notebook)

Some useful options include:

  • unit: Label for counts and rates (default: "it")
  • height: Rows the bar spans (default: 1)
  • gradient: Color the bar with its gradient, with your own list or tuple of "#RRGGBB" colors, or (False) in the terminal's text color (default: True)
  • stream: Where the bar is drawn (default: sys.stderr)
  • notify: Allow desktop notifications about the bar (default: True)

AI Disclosure

psnek was mostly made using Claude Opus 5.5 with checks and balances, both a) as an experiment into seeing how well its design sense can translate into the terminal domain and b) the idea to use Braille subcharacters as a snake for a progress bar came to me after a glass of wine and I wanted to see how well Opus 5.5 could execute it. Opus 5.5 was successful, although it required significant back and forth on the design and a substantial amount of user testing (for example, to catch unexpected visual layout/reflow changes that happen too quickly for agents to automatically catch).

I have reviewed all the code and documentation copy manually in this repository and I stand by it professionally. For any pull requests that are submitted to this repo (particularly by agents), please manually review the output and ensure any changes are high signal and precisely scoped: low-effort PRs that cause more problems than they solve may be closed without comment.

Miscellaneous Notes

  • The terminal GIFs/videos were made using vhs, which apparently Opus 5.5 can use well. An additional postprocessing step was added to crop the resulting video to only salient dimensions in order to cut down on unnecessary space and reduce file size. The demo files and vhs tapes are available in the demo folder.
  • The default gradient colors were chosen to fit two specific criteria: a) sufficient color contrast for both dark and light terminal themes and b) colorblind friendliness against other semantic bar colors. The palette is similar to viridis sans the yellows which don't play well with light backgrounds.
  • A bar is considered stalled once the wait since its last progress is past the smoothed mean of its gaps plus 3 standard deviations (and at least 5 seconds), and stuck past 9 standard deviations (and at least 30 seconds). The first 3 gaps are never flagged, so a slow first step such as loading a model is fine.
  • The ETA drops its ~ only when psnek is 99% confident it is within 10% (or a second). During warm-up, irregular loops, changes in pace, and stalls, it keeps the ~.
  • All text psnek draws is sanitized, so captured output can't move the cursor or spoof the display.

To Be Added

  • CLI support for non-Python pipes (implemented but not tested)
  • Canonical psnek callbacks for all major Python machine learning frameworks (e.g. Hugging Face transformers, Keras)

Maintainer/Creator

Max Woolf (@minimaxir)

Max's open-source projects are supported by his Patreon and GitHub Sponsors. If you found this project helpful, any monetary contributions to the Patreon are appreciated and will be put to good creative use.

License

MIT