=================================== 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 :doc: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)
:py:func:progressbar.progressbar, the iterable-wrapping shortcut, chooses
between two backing classes: the full ProgressBar (widgets, colors, the
whole rendering pipeline) and :py:class:~progressbar.fast.FastProgressBar,
a subclass that skips the widget system entirely and renders through one
fixed formatter (:py:func:~progressbar.fast._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
:py:class:~progressbar.fast.FastProgressBar directly if you want the fast
renderer without going through the shortcut -- see
:doc:../reference/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, :py:meth:ProgressBar.__iter__ <progressbar.bar.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:
.. code:: sh
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 :py:meth:ProgressBar.start() <progressbar.bar.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
:doc: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.