tqdm-tag
tqdm-tag enhances tqdm progress bars by letting you assign a colored status tag to each item as it is processed — making warnings, errors, or any custom outcome immediately visible in the progress bar itself.
Installation
Quick start
TqdmErrorTag is a drop-in replacement for tqdm — swap the class name and you're done. Call .warn() or .error() on any item to color that segment of the bar:
from tqdm_tag import TqdmErrorTag
pbar = TqdmErrorTag(range(100), legend=True, desc="Processing")
for item in pbar:
result = process(item)
if result.has_warning: pbar.warn()
if result.has_error: pbar.error()
With legend=True, a live second line below the bar shows running counts:
\(\texttt{Processing:\ \ 73\%|}\color[RGB]{166,227,161}{\texttt{████████████}}\color[RGB]{249,226,175}{\texttt{███}}\color[RGB]{166,227,161}{\texttt{███}}\color[RGB]{243,139,168}{\texttt{█}}\color[RGB]{170,170,170}{\texttt{████}}\color[RGB]{255,255,255}{\texttt{ |\ 73/100\ [00:03{<}00:01,\ 12.5it/s]}}\)
\(\color[RGB]{249,226,175}{\texttt{█\ warn:\ 4}}\) \(\color[RGB]{243,139,168}{\texttt{█\ error:\ 1}}\)
See the API reference for full class and method documentation.
Features
- Drop-in replacement — swap
tqdmforTqdmErrorTagand add.warn()/.error()calls. No other changes required. Tagdataclass — groups name, color, and status in one object; status auto-assigned if omitted.- Live legend —
legend=Truerenders a second line below the bar with a colored swatch and count per tag, updating in real time. - Dynamic tags — define tags upfront via
tags=[...]or register them on the fly by passing aTagobject to.tag(). - Color support — named colors (
"red","yellow", …) and hex strings ("#ff4444"). - Status reduction — configure how multiple tagged items within one bar segment are combined (
max,min, or any callable). Genuinely mixed segments render as a proportional fg/bg color blend instead of flattening to one winner. - Custom legend —
legend_formataccepts a callable for full control over the legend line. - Resumable bars —
initialandinitial_statuspick a bar back up mid-run, restoring per-item tag history rather than starting the display from scratch. - Retag by index —
.tag(..., index=i)targets a specific item's status; in manual mode the first call for a givenicounts it, a later one on the same slot is a retry that leavesnalone. Useful for retry logic or out-of-order completions.
Examples
TqdmErrorTag with legend
The fastest way to get started — pre-wired with warn (yellow) and error (red):
from tqdm_tag import TqdmErrorTag
pbar = TqdmErrorTag(range(100), legend=True, desc="Processing")
for i in pbar:
if i % 20 == 0: pbar.warn()
if i == 95: pbar.error()
Custom tags with TqdmTag
Use Tag objects to group name, color, and status together. status is auto-assigned if omitted:
from tqdm_tag import Tag, TqdmTag
pbar = TqdmTag(
range(100),
tags=[Tag("warn", color="yellow"), Tag("error", color="red")],
legend=True,
)
for i in pbar:
if i == 10: pbar.tag("warn")
if i == 80: pbar.tag("error")
Note
When total is known, per-item status is stored in a fixed uint8 array (0-255) for memory efficiency, so at most 256 distinct status values are available. Registering a Tag (or calling .tag()) with a status outside that range raises a warning at registration time, ahead of the OverflowError it would otherwise cause once an item is actually tagged with it.
Add tags dynamically
Pass a Tag object to .tag() to register and apply a new tag in one call. Subsequent calls can use the name string:
from tqdm_tag import Tag, TqdmTag
pbar = TqdmTag(range(100))
for i in pbar:
if i == 10: pbar.tag(Tag("warn", color="yellow")) # registers + applies
if i == 30: pbar.tag("warn") # reuse by name
if i == 80: pbar.tag(Tag("error", color="red"))
Turn the whole bar green on success
import time
from tqdm_tag import TqdmTag
items = range(50)
pbar = TqdmTag(items, colour="red")
for i in pbar:
time.sleep(0.05)
if i == len(items) - 1:
pbar.tag("default", color="green")
Customize the legend
Tags are ordered by status value. Pass legend_format to take full control — the callable receives tag_counts (dict of name → count) and tag_to_color (dict of name → color) and must return a string:
def my_legend(counts, colors):
return " ".join(f"[{k.upper()}={v}]" for k, v in counts.items())
pbar = TqdmErrorTag(range(100), legend=True, legend_format=my_legend)
Every item starts out under the implicit "default" tag. Rename it with default_tag_name (e.g. if "default" collides with one of your own tag names), and show its running count in the legend with legend_show_default:
pbar = TqdmTag(
range(100),
tags=[Tag("warn", color="yellow")],
default_tag_name="pending",
legend=True,
legend_show_default=True, # legend now includes "pending: N"
)
for i in pbar:
if i == 10: pbar.tag("warn")
if i == 90: pbar.tag("pending", color="green") # re-color the default tag
Registered tags show up in the legend with a count of 0 until first used. Pass prune_legend=True to hide them until then:
pbar = TqdmTag(
range(100),
tags=[Tag("warn", color="yellow"), Tag("error", color="red")],
legend=True,
prune_legend=True, # "warn"/"error" only appear once tagged at least once
)
Reduce operation for dense bars
When a terminal is narrow, multiple items share one bar segment. Use reduce_op to choose which status wins:
from tqdm_tag import Tag, TqdmTag
pbar = TqdmTag(
range(100),
tags=[Tag("warn", color="yellow", status=1), Tag("error", color="red", status=2)],
reduce_op=max,
reduce_ignore_default=True,
)
for i in pbar:
if i % 10 == 1: pbar.tag("warn")
if i % 10 == 2: pbar.tag("error")
A segment that's genuinely mixed (not every item shares one status) doesn't just flatten to reduce_op's winner — it's reduced a second time over whatever's left, and rendered as a single character split proportionally between the winner's color (foreground) and the runner-up's color (background), so a minority status stays visible instead of being fully outvoted or fully hiding the majority.
Note
Blending only applies with the default Unicode charset; ascii=True bars fall back to the flat winner color, since ASCII has no sub-character glyphs to blend with.
Resume a bar after a restart
Pass initial (the number of items already done) so the bar picks up at the right percentage, and initial_status to restore the previous run's per-item tags instead of showing that prefix as untagged:
from tqdm_tag import Tag, TqdmTag
tags = [Tag("warn", color="yellow", status=1), Tag("error", color="red", status=2)]
# first run got 30/100 done, with items 5 and 12 warned, item 21 errored
pbar = TqdmTag(
range(30, 100),
total=100,
initial=30,
tags=tags,
initial_status={5: 1, 12: 1, 21: 2}, # {index: status}, e.g. loaded from a checkpoint
)
for i in pbar:
...
initial_status also accepts a sparse (2, N) array ([indices, statuses]) or a dense (total,) array giving every item's status directly.
Retag an item by index
Pass index to .tag() to target a specific slot instead of the current item. In manual (non-iterable) mode, whether this advances the bar depends on whether that slot was ever tagged before: the first tag(..., index=i) for a given i counts it (same as index=None would), while a later tag(..., index=i) call on that same slot is treated as a retry and leaves n alone:
from tqdm_tag import Tag, TqdmTag
tags = [Tag("success", color="green", status=1), Tag("error", color="red", status=2)]
pbar = TqdmTag(total=len(items), tags=tags)
for i, item in enumerate(items):
try:
process(item)
pbar.tag("success")
except Exception:
pbar.tag("error") # n advances either way
# later: retry failed items, flip their status without touching n
# (these slots were already tagged above, so this is recognized as a retry)
for i in failed_indices:
if retry(items[i]):
pbar.tag("success", index=i)
index also fits out-of-order completions in manual mode (e.g. iterating concurrent.futures.as_completed directly, not via for x in pbar), where the true slot for a completed item has to be looked up rather than assumed from completion order — the first tag(..., index=true_idx) for a slot both records its status and counts it. Works whether or not total is known.
Caveat: the "already tagged?" check only sees slots that went through .tag() (or initial_status/iteration). If you count an item with a bare pbar.update(1) and never .tag() that slot, it's harmless on its own — but if you later call tag(..., index=that_slot) for the first time, it'll look unseen and advance n again, double-counting it. If you're going to .tag(index=...) a slot at all, route its completion through .tag() from the start (even the plain-success case, e.g. pbar.tag("success", index=i)) rather than mixing in a separate update(1) for the same slot.