Write a custom widget

The built-in widgets cover percentages, timers, and transfer speeds, but a job may also need a named phase. The custom widget below shows “preparing”, “processing”, and “finishing” as the work advances.

The current job phase
"""Name each phase of a job with a custom widget.

A widget returns the text for one redraw from the bar's data snapshot.
Place the phase before the stretching bar so it stays easy to find.
"""

import time

import progressbar
from progressbar.bar import ProgressBarMixinBase
from progressbar.widgets import Data, WidgetBase

STEPS: int = 100


class Stage(WidgetBase):
    """Show the phase that corresponds to the current percentage."""

    def __call__(self, progress: ProgressBarMixinBase, data: Data) -> str:
        percentage: float = data['percentage'] or 0.0
        phase: str
        if percentage < 20:
            phase = 'preparing'
        elif percentage < 85:
            phase = 'processing'
        else:
            phase = 'finishing'
        return f'Phase: {phase:10}'


def main() -> None:
    widgets: list[str | WidgetBase] = [
        Stage(),
        ' ',
        progressbar.Percentage(),
        ' ',
        progressbar.Bar(),
    ]
    bar: progressbar.ProgressBar
    step: int
    with progressbar.ProgressBar(max_value=STEPS, widgets=widgets) as bar:
        for step in range(STEPS):
            time.sleep(0.02)
            bar.update(step + 1)


if __name__ == '__main__':
    main()

The phase appears before the bar. Stage returns a label padded to ten characters, so switching from “preparing” to “processing” keeps the percentage and bar aligned.

A widget is any callable matching WidgetBase.__call__(self, progress, data), returning the text to render for one redraw. Subclass WidgetBase and implement __call__: progress is the bar itself (read from it, don’t mutate it), and data is the same snapshot dict the built-in widgets read – data['value'], data['percentage'], and so on. Drop the instance straight into a widgets= list alongside the built-ins. Nothing distinguishes a custom widget from a shipped one at that point.