Progress bars, spinners and other widgets from rich made more intuitive, including more sensible defaults.
rich is a very powerful terminal rendering library that allows creating beautiful outputs. While it already includes progress bars and spinners, they can be a pain to use. For example, when you want to combine multiple widgets at the same time, you will have to manually manage one Live widget and group everything into that one, otherwise it will raise an error. This is not very intuitive at all, and that could be managed automatically without the user having to think about it.
That is exactly what this library provides. It uses the existing rich widgets and juggles them around to be displayed in a single Live widget. In 99% of the cases you'll be able to just create your progress widgets and they will be stacked automatically (similar to tqdm).
The API of rich is somewhat clunky for such simple widgets and they use some questionable defaults. As an example, the default progress bar (over an iterator) is not adapted to the current terminal width, only shows the percentage of the progress and the estimate. No time elapsed, no info on number of iterations and the estimate is calculated based on a single iteration, rather than taking into account the total time elapsed so far, which is severely inaccurate if not every iteration takes the exact same time. To make matters worse, if an iteration takes longer than 30 seconds, it will not show any estimate at all, making it completely useless.
It is absolutely incomprehensible why they chose these defaults, especially since tqdm already existed before it, which has much more sensible defaults that stood the test of time.
Progrich is available on PyPI and can be installed like any other dependency.
uv is recommended for managing the dependencies of your project.
uv add progrichpip install progrichfrom progrich import ProgressBar
pbar = ProgressBar("Basic example", total=20)
# Start the progress, which displays it.
pbar.start()
for i in range(20):
# Do some work
...
# Advance the progress bar by one step (optionally, specify by how many steps it is advanced)
pbar.advance()
# Finish the progress, which also hides it unless the ProgressBar was created with persist=True.
pbar.stop()Starting and stopping the progress bar is a common operation and therefore ProgressBar implements a context manager,
which means it can be used in a with statement, greatly simplifying the usage.
from progrich import ProgressBar
with ProgressBar("`with` example", total=20) as pbar:
for i in range(20):
# Do some work
...
# Advance the progress bar by one step (optionally, specify by how many steps it is advanced)
pbar.advance()A common operation is to show a progress bar for any iterable. This is supported with the ProgressBar.iter() method,
which takes the same argument as the constructor, except that it takes a collection (iterable whose size is known)
instead of a total number of steps.
from progrich import ProgressBar
for i in ProgressBar.iter(range(20), desc="iter example"):
# Do some work
...This is similar to tqdm, but instead of having an overloaded function, it is a separate method to make it more explicit.
Creating multiple progress bars will work out of the box, however due to some components having a dynamic width, they are not as nicely aligned.
To alleviate the alignment issue, you can provide an existing progress widget which will share the underlying progress display in order to combine them into a uniform representation.
from progrich import ProgressBar
# The progress for the total training, which will be persisted once the training
# finishes, as it might be helpful to see the total time it took at the end.
pbar_total = ProgressBar("Total", total=5, persist=True, prefix="[Training]")
pbar_total.start()
# Train for 5 epochs
for i in range(5):
# Each epoch runs throw the whole training dataset
for batch in ProgressBar.iter(
train_dataset,
desc="Train Set",
prefix=f"Epoch {i + 1}",
# Share the same progress display!
progress=pbar_total,
):
# Training loop
...
# Evaluate current model at the end of each epoch
for batch in ProgressBar.iter(
validation_dataset,
desc="Validation Set",
prefix=f"Epoch {i + 1}",
# Share the same progress display!
progress=pbar_total,
):
# Evaluation loop
...
# Epoch ends, so increment the total progress.
pbar_total.advance()
pbar_total.stop()By specifying progress=pbar_total they two progress bars will be aligned more nicely.
The spinner follows the same general API as progress bar, except that there is no count as it is an undetermined progress indicator.
The most common use case will be showing a spinner when launching a long running operation with no real progress
indication to let the user know that something is happening. For this, using a spinner in a with statement is ideal.
from progrich import Spinner
with Spinner("Waiting for server to start..."):
server = start_server()It might be helpful to finish the spinner with a message that shows whether the longer running operation succeeded or
failed or to just keep a trace of finished tasks. The methods Spinner.success() and Spinner.fail() do exactly that
and show the provided message with a coloured icon.
from progrich import Spinner
with Spinner("Waiting for server to start...") as spinner:
try:
server = start_server()
# Finishes with a message like: ✔ Server started on port 3000
spinner.success(f"Server started on port {server.port}")
except OSError as e:
# Finishes with a message like: ✖ Server failed to start with error: port 3000 already in use
spinner.fail(f"Server failed to start with error: {e}")If no message is provided the message from the spinner will be used instead. You can change the icon that is displayed
by adding the icon= argument.
An interactive table can be displayed where you can keep adding new rows and it will be limited to display the last few rows. This makes it easy to show how the values evolve with a bit of context around them but not keeping too much information from the past.
with Table(["Epoch", "Loss", "Accuracy"]) as table:
for i in range(5):
loss, accuracy = train_epoch(i)
table.insert_row([i, loss, accuracy])In order to combine multiple widgets seamlessly, a Manager is used where each of the widgets is added. A default
(global) manager is used automatically if no manager is specified manually. So in most cases you don't need to worry
about creating one yourself.
If you want to customise certain aspects of the manager, you have two options:
- Create a
Manageryourself and pass it to any widget you create. - Modify the default manager.
You can retrieve the default manager with Manager.default().
For example, if you run a distributed program and want to only show the progress in the main process, you can disable the console for all other processes.
from rich.console import Console
from progrich import Manager
manager = Manager.default()
if not is_main_process:
# Use a console with quiet=True, so that there is no output.
manager.set_console(Console(quiet=True))You can easily implement your own custom widget by subclassing ManagedWidget.
The only required method to implement is def __rich__(self) -> RenderableType, which returns any rich-compatible
renderable that will be displayed.
If you plan to use an existing dynamic rich widget, it is as simple as storing that widget in your class and returning
it from the __rich__ method. Everything else is handled for you!
You might also need to customise the start() and stop() methods, but for everything you are free to any custom
methods or attributes that facilitate handling the state of your widget.
Take a look at the provided widgets, such as ProgressBar in src/progrich/pbar.py or
Spinner in src/progrich/spinner.py to get a better understanding.