Performance and the fast path

Wrapping a loop in a progress bar should not be the reason the loop gets slower. progressbar2 has three layers aimed at that: an always-on update gate, an automatic lean renderer, and an opt-in native iterator. This page covers what each one does, when it applies, and its measured cost.

Layer 1: the integer gate (always on)

Every bar, fast or full, is protected by the same cheap integer-threshold check described in Rendering and the update gate: most update() calls (or iterations, when wrapping an iterable) do nothing more than an increment, a comparison, and a store, only re-entering the real redraw logic once a calibrated threshold is crossed. This is why the “default” cost below is already very low before any opt-in feature is involved.

Layer 2: FastProgressBar (automatic, opt-out)

progressbar.progressbar(), the iterable-wrapping shortcut, chooses between two backing classes: the full ProgressBar (widgets, colors, the whole rendering pipeline) and FastProgressBar, a subclass that skips the widget system entirely and renders through one fixed formatter (_pure_format_fast_line()) built directly from the bar’s state. It reuses everything else – the update gate, start/finish, stream redirection, resizing – just not per-widget formatting.

The fast class is chosen automatically when all of the following hold: no explicit widgets= was given, fast was not passed as False, variables is empty or not given, unit is still the default 'it', unit_scale is off, postfix is unset, and the PROGRESSBAR_DISABLE_FASTPATH environment variable is not set. In other words: the common “just wrap my loop” case gets the lean renderer by default. Anything that needs the widget system (a custom widget list, tqdm’s unit/postfix extras, dynamic Variable widgets) automatically gets the full ProgressBar instead. You can also construct FastProgressBar directly if you want the fast renderer without going through the shortcut – see ProgressBar.

This dispatch only happens inside progressbar.progressbar(). Constructing ProgressBar() yourself always gets the full class.

Layer 3: the native iterator (opt-in via the fast extra)

Separately, ProgressBar.__iter__ checks whether the optional speedups package (speedups.progressbar.FastBarIterator) is importable. If it is, and PROGRESSBAR_DISABLE_FASTPATH is not set, iterating over any bar – fast or full – dispatches to that native iterator instead of the pure-Python generator. It counts items itself and calls back into the bar (_fast_begin/_fast_tick/_fast_end/_fast_end_dirty) only at gate crossings, so the per-iteration counting that Layer 1 already made cheap in pure Python gets cheaper still.

speedups is not installed by default. Install it with the fast extra:

pip install progressbar2[fast]

PROGRESSBAR_DISABLE_FASTPATH is a single switch for all three layers above Layer 1: it stops progressbar.progressbar() from choosing FastProgressBar, stops __iter__ from choosing the native iterator, and disables the integer gate itself in ProgressBar.start() – with it set, every bar calls update() on every single iteration, matching the pre-gate behavior exactly. That makes it a genuine “turn all of this off” switch, useful when debugging a rendering issue you want to rule the gate out of, not just an opt-out of the optional native accelerator.

Measured cost

The following is reproduced from this repository’s own benchmark suite (benchmarks/report.md, generated by python benchmarks/bench.py && python benchmarks/report.py, which also compares against other libraries). Figures are from one measurement environment (CPython 3.13.12, macOS arm64, output to a real pty) and will vary by machine – re-run the benchmark rather than treating these as guarantees:

  • Default iterator-wrap overhead (wrapping a tight loop with default settings, 1,000,000 iterations): ~5.1 ns/iteration on top of the bare loop. This is Layer 1 doing its job – almost no per-iteration work beyond the gate check itself.

  • Forced per-update render cost (throttling disabled, every update actually redraws): ~25.5 microseconds per redraw for the full ProgressBar, versus ~4.96 microseconds for the fast-default path (progressbar2-fast in the report) – roughly 5x cheaper once you bypass the widget system.

  • Cold import time: ~1.5 ms, net of bare-interpreter startup – relevant if the library is imported by a short-lived CLI.

Why both the per-iteration number and the per-redraw number matter: with the default min_poll_interval capping redraws at roughly 20/second (see Rendering and the update gate), the expensive render number is paid rarely in practice, and the cheap per-iteration gate cost dominates total overhead for any loop running faster than the redraw cap.

When to reach for which layer

  • Wrapping a simple loop with progressbar.progressbar(iterable) and no custom widgets already gets Layer 1 and Layer 2 for free.

  • Passing widgets=, variables=, unit_scale=True, or a postfix opts back into the full ProgressBar – reasonable, since those features need the widget system that FastProgressBar skips.

  • Install the fast extra when the iteration count itself is large enough (hundreds of millions of items) that even Layer 1’s per-iteration cost is worth shaving further. For most scripts it isn’t necessary.