mrkeyoor.com_
Wed 05 Aug 23:12 UTC
PyPICLI & Toolingupdated 05 Aug 2026

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.

Verdict

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.

API stability5/5The 4.x line has been current since 2015 with no breaking rewrite; code written a decade ago still runs, and the maintainers treat overhead and API as frozen contracts.
Docs4/5The README is a genuinely complete manual (parameters, CLI, integrations, FAQ) and tqdm.github.io mirrors it; discoverability inside that one giant page is the weak spot.
Maintenance4/5Pushed July 2026 with 4.70.0 current; the project is mature with a slow but steady release cadence, and long-open issues around notebooks persist for years.
Ecosystem5/5151M weekly downloads, first-party pandas/keras/dask hooks, contrib modules for concurrency and logging, and wrappers in most ML tooling; it is infrastructure at this point.

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
Skip it if

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.tgz

pip 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 work

Wraps 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 TTY

disable=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

PackageRegistryPick it when
richPyPIYou want progress bars as part of a broader pretty-terminal toolkit (tables, syntax highlighting, live layouts) and control over the look.
alive-progressPyPIYou want flashier animated bars with pause support and do not mind a more opinionated API.
progressbar2PyPIYou are on legacy code that already uses it; it is maintained, just slower and less ubiquitous than tqdm.