Stack independent bars without MultiBar

Not every group of concurrent bars is a single managed job set – sometimes each bar only needs to know its own row in a fixed stack, with no shared start/stop lifecycle tying them together. Reach for line_offset on plain ProgressBar instances instead of Track several jobs at once with MultiBar when that’s the shape of the problem.

Manual line-offset bars
"""Several independently updating bars stacked by hand, without `MultiBar`.

Reach for `line_offset` on plain `ProgressBar` instances -- instead of
`MultiBar` -- when the bars are not a single managed group: each one here
only knows its own row in the stack, not the others. Print the blank lines
first to reserve the rows, since `line_offset` moves the cursor relative to
wherever it already is.
"""

import random
import time

import progressbar

random.seed(0)

BARS = 4
STEPS = 20


def main() -> None:
    print('\n' * BARS, end='')
    bars = [
        progressbar.ProgressBar(
            max_value=STEPS,
            line_offset=index + 1,
            max_error=False,
        )
        for index in range(BARS)
    ]
    for _ in range(STEPS * BARS):
        random.choice(bars).increment()
        time.sleep(0.01)

    for bar in bars:
        bar.finish()
    print()


if __name__ == '__main__':
    main()

Each bar gets its own line_offset= (counting rows up from the current cursor position) and renders independently – there is no shared container or background thread. Print the blank lines the bars will occupy before creating them, since every redraw moves the cursor up by line_offset, writes, then moves back down by the same amount, relative to wherever the cursor already is.

Caveats

That relative cursor movement means anything else that writes to the same stream while these bars are active – a stray print(), a log line, another bar’s redraw landing at the wrong moment – shifts the baseline every bar measures its offset from, and throws off every row after that point. Keep the region these bars occupy free of unrelated output for as long as they’re active.

Because independent bars don’t coordinate a shared target the way MultiBar jobs do, a single call site incrementing a random bar past its own max_value is easy to trigger by accident. Pass max_error=False so an over-target update clamps at max_value instead of raising.