tqdm
tqdm turns any Python iterable into one with a live progress bar: wrap it in tqdm() and you get rate, ETA, and a smart-updating bar on stderr with about 60ns of overhead per iteration. It also works as a shell command for piping, hooks into pandas as progress_apply, has notebook widgets, and ships integrations for keras, dask, and concurrent maps. It is the default answer to 'how long will this loop take' in Python, which its 150M weekly downloads reflect.
The lowest-friction progress bar in any language: one wrapper call, tiny overhead, works from scripts to notebooks to shell pipes. Use it by default in scripts and data jobs; use rich if you already depend on it for terminal UI, and use neither in production log streams.
Use it if
- You have any loop longer than a few seconds in a script or notebook: one wrapped iterable is the entire integration cost
- You want progress on shell pipelines: 'tar -zcf - docs/ | tqdm --bytes --total ...' gives byte rates on anything that pipes
- You need progress over pandas operations: tqdm.pandas() adds progress_apply and progress_map to DataFrames
- You parallelize with process pools and want a bar for free via tqdm.contrib.concurrent's process_map and thread_map
- Your app already uses rich: rich.progress does prettier bars plus tables and spinners in one dependency, and mixing the two fights over the terminal
- The output goes to log files or a service: progress bars are carriage-return spam in logs; log progress at intervals instead, or pass disable=None so tqdm auto-disables on non-TTY
- You need bulletproof Jupyter rendering without extra deps: the notebook widget needs ipywidgets, and version mismatches produce broken blank widgets often enough to be a known pain
- You print from inside the loop: naive print() breaks the bar, and if you will not adopt tqdm.write, the bar will just look glitchy
Setup reality
pip install tqdm, wrap the loop, done: pure Python, and the only conditional dependency is colorama on Windows. The honest problems come later: printing inside a loop tears the bar unless you switch to tqdm.write; nested loops need position handling or they overwrite each other; notebooks need ipywidgets for tqdm.notebook and mismatched jupyter/ipywidgets versions render dead grey boxes; and multiprocess bars need each worker given an explicit position. None of this is hard, all of it is stuff the happy-path demo does not mention.
Patterns
Wrap any iterablewrap-iterable
from tqdm import tqdm
for record in tqdm(records):
process(record)
# with a label and known total for generators:
for chunk in tqdm(stream(), total=n_chunks, desc="ingest"):
handle(chunk)tqdm gets the total from len() when it can; generators show a count and rate but no ETA unless you pass total. Output goes to stderr by default, so redirected stdout stays clean.
Manual bar for non-loop workmanual-updates
from tqdm import tqdm
with tqdm(total=len(files), unit="file") as bar:
for f in files:
upload(f)
bar.update(1)
bar.set_postfix(current=f.name)Use the context manager form so the bar closes on exceptions; a bar left open keeps the line hostage. update() takes any increment, handy for byte counts with unit='B', unit_scale=True.
trange instead of rangetrange-shortcut
from tqdm import trange
for epoch in trange(50, desc="epochs"):
train_one_epoch()trange(n) is exactly tqdm(range(n)) and reads better in ML training loops, which is where most people meet it.
Print without destroying the barprint-inside-loop
from tqdm import tqdm
for item in tqdm(items):
if item.suspicious:
tqdm.write(f"flagged: {item.id}")Plain print() interleaves with the bar's carriage returns and shreds the display. tqdm.write prints the message above the bar and redraws it; this is the single most useful non-obvious API.
Nest inner and outer barsnested-bars
from tqdm import trange
for i in trange(4, desc="outer", position=0):
for j in trange(100, desc="inner", position=1, leave=False):
step(i, j)Without position each bar fights for the same line. leave=False clears the inner bar on completion; on Windows terminals nested rendering is still the flakiest part of tqdm.
Progress bars for pandas applypandas-progress
import pandas as pd
from tqdm import tqdm
tqdm.pandas(desc="scoring")
df["score"] = df["text"].progress_apply(model.score)
# also: df.groupby("user").progress_apply(...)tqdm.pandas() monkey-patches DataFrame/Series with progress_apply and progress_map. Call it once per process; it does not speed anything up, it just tells you how slow apply really is.
Notebook-safe importnotebook-auto
from tqdm.auto import tqdm
for batch in tqdm(loader, desc="train"):
step(batch)tqdm.auto picks the widget bar in Jupyter and the text bar elsewhere, so library code should always import from tqdm.auto. The widget path needs ipywidgets>=6; a grey box with no bar means the jupyter/ipywidgets versions disagree.
Progress for shell pipelinescli-pipe
# count lines with throughput:
seq 9999999 | tqdm --bytes | wc -l
# progress on a tarball with a known total:
tar -zcf - docs/ | tqdm --bytes --total $(du -sb docs/ | cut -f1) > backup.tgzpip installing tqdm gives you the tqdm command. It passes stdin through to stdout and draws the bar on stderr, so it drops into any existing pipe without changing the data.
Progress over process poolsparallel-map
from tqdm.contrib.concurrent import process_map, thread_map
results = process_map(transform, items, max_workers=8, chunksize=16)
# thread_map for I/O-bound workWraps concurrent.futures with a correctly-updating bar, replacing the broken pattern of wrapping pool.imap yourself. chunksize matters: without it, tiny tasks spend more time in IPC than work.
Silence bars in logs and CIauto-disable-non-tty
from tqdm import tqdm
for row in tqdm(rows, disable=None):
load(row)
# disable=None: bar only renders when stderr is a TTYdisable=None (not False) is the deploy-friendly setting: interactive runs get a bar, cron logs and CI output stay clean. Hardcoded disable=True/False are both wrong somewhere.
Tame refresh overhead and flickerrate-limited-updates
from tqdm import tqdm
for x in tqdm(xs, mininterval=1.0, miniters=1000, smoothing=0.1):
fast_op(x)For very hot loops the default 0.1s refresh can dominate cheap iterations; mininterval/miniters throttle redraws. smoothing near 0 gives average-rate ETA instead of jumpy instantaneous estimates.
Byte progress for downloadsdownload-progress
import requests
from tqdm import tqdm
resp = requests.get(url, stream=True)
total = int(resp.headers.get("content-length", 0))
with open(dest, "wb") as f, tqdm(total=total, unit="B", unit_scale=True, unit_divisor=1024) as bar:
for chunk in resp.iter_content(chunk_size=65536):
f.write(chunk)
bar.update(len(chunk))unit_scale with unit_divisor=1024 prints human MiB/GiB rates. When content-length is missing, total=0 shows a count-only bar rather than percentages.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| rich | PyPI | You want progress bars as part of a broader pretty-terminal toolkit (tables, syntax highlighting, live layouts) and control over the look. |
| alive-progress | PyPI | You want flashier animated bars with pause support and do not mind a more opinionated API. |
| progressbar2 | PyPI | You are on legacy code that already uses it; it is maintained, just slower and less ubiquitous than tqdm. |