Python progress bars

tqdm in Python: what it is and how to use it

tqdm is a Python library that turns any loop into a progress bar with a count, percentage, elapsed time, ETA and rate. Install it with pip install tqdm (version 4.70.1, Python 3.8+) and wrap the iterable, as in for x in tqdm(items):. The bar redraws at most every 0.1 seconds on stderr, so it costs very little.

5 min readtqdm and progress bars

At a glance

  • Install with pip install tqdm or conda install -c conda-forge tqdm. The latest release is 4.70.1 (2026-09-11) and needs Python 3.8+.
  • Wrap any iterable: for x in tqdm(items):. trange(n) is shorthand for tqdm(range(n)).
  • The bar goes to stderr and redraws at most every 0.1 seconds (mininterval=0.1).
  • Inside a loop, print with tqdm.write() instead of print() so the bar stays on one line.
  • TQDM_DISABLE=1 turns every bar off without code changes (tqdm 4.66.0 and newer).

What tqdm is

tqdm is a progress bar library for Python. The name comes from the Arabic word *taqaddum*, which can mean "progress". When you pass a list, range or generator into tqdm(), it yields your items as normal while it updates a text bar on sys.stderr.

Out of the box the bar shows five things:

  • Percentage complete
  • Current count out of the total
  • Elapsed time
  • Estimated time remaining (ETA)
  • Processing rate

If your iterable has no len() and you don't pass total, tqdm can't know when the loop will finish, so it drops the percentage and ETA and shows a running counter and rate. The package is dual-licensed MPL-2.0 AND MIT and has about 31,400 stars on GitHub.

Install tqdm and wrap your first loop

Install tqdm from PyPI with pip, or from the conda-forge channel with conda. Both carry 4.70.1.

Bash
pip install tqdm
# or
conda install -c conda-forge tqdm

Import tqdm and wrap it around whatever you iterate over. For range(n) you can import trange(n), a built-in shorthand for tqdm(range(n)).

Python
from time import sleep
from tqdm import tqdm, trange

for name in tqdm(["a.csv", "b.csv", "c.csv"], desc="files"):
    sleep(0.5)

for step in trange(100, desc="steps"):
    sleep(0.01)

The parameters you will actually use

A basic loop needs no configuration: tqdm reads len(iterable) when it exists and redraws at most every 0.1 seconds, and at least every 10 seconds. When you want a label, byte sizes or bars that clean up after themselves, these are the parameters that matter, with their real defaults in 4.70.1:

ParameterDefaultWhat it does
descNoneLabel printed before the bar
totalNoneExpected iterations; len(iterable) is used when it exists
leaveTrueKeeps the finished bar on screen
fileNone (stderr)Stream the bar is written to
mininterval0.1Minimum seconds between redraws
maxinterval10.0Maximum seconds between redraws
minitersNoneMinimum iterations between redraws, tuned automatically
disableFalseTrue hides the bar; None hides it when output is not a terminal
unit"it"Name of one iteration, used in the rate
unit_scaleFalseTrue adds k, M and G prefixes to large numbers
unit_divisor1000Divisor used by unit_scale; set 1024 for bytes
smoothing0.3Rate averaging, from 0 (overall average) to 1 (latest speed)
positionNoneLine offset for the bar, automatic by default
delay0Seconds to wait before the bar first appears

Since tqdm 4.66.0, you can also override defaults from the shell with TQDM_ environment variables. Setting TQDM_MININTERVAL=5 or TQDM_DISABLE=1 changes every bar in your script without touching the code.

Manual updates: total, update() and set_postfix

Not every task is a clean for loop over a list. When you read chunks from a stream, step through a while loop or run training batches, open tqdm in a with block and drive it yourself.

Inside the block, three methods do the work:

  • bar.update(n) adds n to the counter.
  • bar.set_postfix(loss=0.12) appends key=value stats to the right of the bar.
  • bar.set_description("text") changes the label on the left while the bar runs.
Python
from time import sleep
from tqdm import tqdm

with tqdm(total=50, desc="training") as bar:
    for step in range(50):
        sleep(0.02)
        loss = 1 / (step + 1)
        bar.set_postfix(loss=f"{loss:.3f}")
        bar.update(1)
    bar.set_description("done")

For downloads or streams where you count bytes instead of items, set unit="B", unit_scale=True and unit_divisor=1024. tqdm then formats the raw byte counts as KB, MB or GB as you call update().

Python
import urllib.request
from tqdm import tqdm

url = "https://www.python.org/ftp/python/3.13.0/Python-3.13.0.tgz"
with urllib.request.urlopen(url) as resp, open("python.tgz", "wb") as out:
    size = int(resp.headers.get("Content-Length", 0)) or None
    with tqdm(total=size, unit="B", unit_scale=True, unit_divisor=1024, desc="download") as bar:
        while chunk := resp.read(64 * 1024):
            out.write(chunk)
            bar.update(len(chunk))

Printing, nested bars and files

A plain print() inside a tqdm loop breaks the bar, because your text and the bar fight over the same terminal line. Use tqdm.write(s, file=None, end="\n", nolock=False) instead: it prints your line above the bar and redraws the bar underneath.

Python
from time import sleep
from tqdm import tqdm

for job in tqdm(range(5), desc="jobs"):
    sleep(0.3)
    tqdm.write(f"finished job {job}")

When you nest one tqdm loop inside another, the inner bar leaves a finished 100% line behind on every outer step, because leave=True is the default. Pass leave=False to the inner bar so it clears itself, and let position=None handle the line offsets.

Python
from time import sleep
from tqdm import tqdm

for epoch in tqdm(range(3), desc="epochs"):
    for batch in tqdm(range(40), desc="batches", leave=False):
        sleep(0.01)

If you read a file object directly, tqdm.wrapattr(file, "read", total=size) wraps its read method so you don't have to call update() by hand. It counts the bytes read and sets unit="B", unit_scale=True and unit_divisor=1024 for you.

Python
import hashlib
import os
from tqdm import tqdm

path = "python.tgz"
digest = hashlib.sha256()
with open(path, "rb") as fh:
    with tqdm.wrapattr(fh, "read", total=os.path.getsize(path), desc="sha256") as f:
        while chunk := f.read(1024 * 1024):
            digest.update(chunk)
print(digest.hexdigest())

pandas, notebooks and worker pools

For pandas, call tqdm.pandas() once and it registers progress_apply and progress_map on Series and DataFrames. Swap .apply() for .progress_apply() and you get a bar over the operation. The pandas progress_apply guide covers group-by and the pandas 3 fixes.

If the same code runs in Jupyter and in a terminal, import with from tqdm.auto import tqdm. It picks the Jupyter widget bar inside a notebook and the text bar everywhere else; the Jupyter guide covers the widget setup.

For worker pools you don't need callbacks around concurrent.futures. tqdm.contrib.concurrent ships process_map and thread_map, which run a function across a process or thread pool and draw one bar while they do. The multiprocessing guide covers chunksize and Pool.imap.

See a tqdm bar from another device

A tqdm bar lives in one terminal. If the job runs on a server or a GPU box, you only see it while you're logged in.

FAQ

Is tqdm free for commercial use?

Yes. tqdm is licensed MPL-2.0 AND MIT, and both licences allow commercial and private use. MPL-2.0 only asks you to share changes you make to tqdm's own MPL-licensed files if you distribute them.

Does tqdm slow down my loop?

Barely. In our test on Python 3.13 with tqdm 4.70.1, an empty 10-million-step loop took 0.55 seconds plain and 2.21 seconds with tqdm, about 166 nanoseconds extra per iteration. Any loop body that does real work hides that cost, and the bar redraws at most every 0.1 seconds.

Why does tqdm print to stderr instead of stdout?

The default output stream is sys.stderr. That keeps your stdout clean, so you can pipe your script's real output to another program or a file without progress bar characters mixing into the data.

How do I turn tqdm off in CI or production?

Set the environment variable TQDM_DISABLE=1 to silence every bar without touching your code (tqdm 4.66.0 and newer), or TQDM_MININTERVAL=5 if you only want fewer log lines. In code, disable=True turns a bar off and disable=None turns it off whenever the output is not a terminal.

What do it/s and s/it mean?

They show your loop's rate in the default unit, it (iterations). When the loop runs fast, tqdm shows it/s, iterations per second; when each item takes longer than a second, it flips to s/it, seconds per iteration.

Check on long runs from your phone

Change one import and every tqdm bar prints a link you can open on any device. Get a Discord or Slack message when the run finishes, fails or stalls.

Get a live linkpip install tqdm-live

Sources