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-fastin 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 apostfixopts back into the fullProgressBar– reasonable, since those features need the widget system thatFastProgressBarskips.Install the
fastextra 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.