Python progress bars

Progress bars for pandas apply with tqdm

To get a progress bar on pandas operations, call tqdm.pandas() once and replace .apply() with .progress_apply(). We tested this on 2026-10-30 using pandas 3.1.0 (released 2026-10-22, requires Python 3.11+) and tqdm 4.70.1. If your code broke after upgrading to pandas 3, you need at least tqdm 4.67.2.

5 min readtqdm and progress bars

At a glance

  • tqdm.pandas() adds progress_apply and progress_map; swap .apply() for .progress_apply().
  • pandas 3 needs tqdm 4.67.2 or newer; older versions fail with AttributeError: 'Series' object has no attribute '_is_builtin_func'.
  • progress_applymap fails on pandas 3 because pandas removed applymap; use progress_map instead.
  • Pass extra arguments with args=(...) or keywords, because tqdm drops bare positional arguments.

Add progress_apply in two lines

Import tqdm, run tqdm.pandas(), and call .progress_apply() on your Series instead of .apply(). Each run creates a fresh progress bar and closes it automatically when the operation finishes.

Python
import pandas as pd
from tqdm import tqdm

tqdm.pandas(desc="squaring")

s = pd.Series(range(100_000))
result = s.progress_apply(lambda x: x ** 2)

Whatever keyword arguments you pass into tqdm.pandas(**kwargs), like desc or ncols, get passed along to every progress bar it makes. Under the hood, that single setup call attaches methods across pandas:

  • progress_apply on Series, DataFrame, SeriesGroupBy, DataFrameGroupBy, Rolling, and Expanding
  • progress_map on Series and DataFrame
  • progress_aggregate and progress_transform on GroupBy
  • progress_applymap on DataFrame, which no longer works on pandas 3

If you're working in a Jupyter notebook, import tqdm from tqdm.auto before calling tqdm.pandas() so you get the notebook widget bar instead of plain text output. The Jupyter guide covers the widget setup.

Rows, columns and progress_map

On a DataFrame, df.progress_apply() works along whichever axis you pick. With axis=0 (the default), the bar counts columns, and with axis=1, it counts rows. For element-wise work on a Series, you can also use s.progress_map(), which works as expected on pandas 3.1 with tqdm 4.70.1.

Python
import pandas as pd
from tqdm import tqdm

tqdm.pandas()

df = pd.DataFrame({"a": range(10_000), "b": range(10_000)})
df["total"] = df.progress_apply(lambda row: row["a"] + row["b"], axis=1)
df["label"] = df["a"].progress_map(lambda x: f"id-{x}")

Element-wise mapping on a whole DataFrame is a different story. pandas 2.1 deprecated DataFrame.applymap in favour of DataFrame.map, and pandas 3 removed applymap entirely, so df.progress_applymap() shows a FutureWarning on pandas 2.1 and 2.2 and fails with AttributeError: 'DataFrame' object has no attribute 'applymap' on pandas 3.

Use df.progress_map(func) instead. It returns the right results, but because of open tqdm issue #1798 the bar sets its total to the number of columns rather than the number of cells. In our test on 3 columns of 200 rows, the bar showed 100% after 1.2 seconds of a 5.5-second job.

Group-by progress bars

When you group a DataFrame or a Series, df.groupby("g").progress_apply(f) and SeriesGroupBy.progress_apply(f) track progress across the groups, so the bar counts the number of groups. progress_transform works the same way, and progress_aggregate does too as long as you pass a function: a string name such as "sum" raises TypeError: 'str' object is not callable.

Python
import pandas as pd
from tqdm import tqdm

tqdm.pandas(desc="groups")

df = pd.DataFrame({"g": [i % 50 for i in range(10_000)], "v": range(10_000)})
means = df.groupby("g").progress_apply(lambda part: part["v"].mean(), include_groups=False)
sums = df.groupby("g")["v"].progress_aggregate(lambda values: values.sum())

Notice how the grouping columns behave if you're moving from pandas 2 to pandas 3. On pandas 2.2, df.groupby("g").progress_apply(f) passes the grouping column into f and gives you a DeprecationWarning, whereas pandas 3 leaves the grouping columns out and doesn't warn at all. Passing include_groups=False, as above, gives the same result without a warning on both. Also avoid calling groupby(...).progress_map: pandas GroupBy objects don't have a map method, so it raises an AttributeError (tracked in open tqdm issue #1803).

What works on pandas 3

Here is how the registered methods behaved when we tested them on pandas 3.1.0 with tqdm 4.70.1:

MethodWorks on pandas 3.1.0What the bar counts
Series.progress_apply(f)YesElements
DataFrame.progress_apply(f, axis=1)YesRows
DataFrame.progress_apply(f)YesColumns
Series.progress_map(f)YesElements
DataFrame.progress_map(f)Yes, results are correctColumns, so it hits 100% early (#1798)
GroupBy.progress_apply(f)YesGroups
GroupBy.progress_transform(f)YesGroups
GroupBy.progress_aggregate(f)Only with a function, not "sum"Groups
Rolling or Expanding .progress_apply(f)YesCalls, with no total
GroupBy.progress_map(f)No, AttributeError (#1803)Nothing
DataFrame.progress_applymap(f)No, AttributeErrorNothing

Passing arguments and speed

tqdm drops extra positional arguments after the function: it prints a TqdmDeprecationWarning line and calls your function without them, which usually ends in a TypeError. Pass them with args=(...) or as keyword arguments instead.

Python
import pandas as pd
from tqdm import tqdm

tqdm.pandas()

def add(x, amount):
    return x + amount

s = pd.Series(range(1_000))
s.progress_apply(add, args=(10,))
s.progress_apply(add, amount=10)

The progress bar adds a small Python call per row, but the actual bottleneck is apply itself calling a Python function on every row, which you can skip by using vectorised pandas operations.

Errors after upgrading pandas

Upgrading to pandas 3.0 broke tqdm's integration for anyone on tqdm 4.67.1 or older. Running progress_apply with those older versions on pandas 3 fails with errors like AttributeError: 'Series' object has no attribute '_is_builtin_func' (tqdm issue #1701).

This was fixed in tqdm 4.67.2, released on 2026-01-30. Upgrade tqdm to fix it:

Bash
pip install -U "tqdm>=4.67.2"

Watch a long apply from another device

If you want to check on a slow progress_apply job from your phone, Runpeek's tqdm-live package streams your existing tqdm bar to a browser without changing how the bar renders in your terminal. Call import tqdm_live; tqdm_live.patch() before tqdm.pandas(), or swap your import to from tqdm_live import tqdm, and each bar prints a runpeek ▶ https://runpeek.app/b/<uuid> link. You don't need an account, and links expire 30 minutes after the last update.

FAQ

Why does progress_apply raise AttributeError on pandas 3?

pandas 3.0 broke compatibility with tqdm 4.67.1 and older, leading to errors like AttributeError: 'Series' object has no attribute '_is_builtin_func' (tqdm issue #1701). Run pip install -U "tqdm>=4.67.2" to get the fix released on 2026-01-30. If the error says 'DataFrame' object has no attribute 'applymap', you're calling df.progress_applymap(), which broke because pandas 3 removed applymap.

How do I use progress_apply in a Jupyter notebook?

Import tqdm from tqdm.auto before you run tqdm.pandas(). That tells tqdm to use the notebook widget bar when you call progress_apply.

Does progress_apply make apply slower?

A little. In our test on 1,000,000 rows with a trivial function, apply took 1.24 seconds and progress_apply took 1.99 seconds, because tqdm wraps every call. With real work in the function the difference shrinks, and vectorised pandas operations avoid apply completely.

Why does my DataFrame.progress_map bar hit 100% straight away?

df.progress_map(func) sets its progress bar total to the number of columns instead of the number of cells, so it reaches 100% long before the work is done even though the result is correct. This is an open bug tracked in tqdm issue #1798; progress_map on a single column (a Series) counts correctly.

How do I pass arguments to the function in progress_apply?

Use args=(10,) or keyword arguments such as amount=10. tqdm drops bare positional arguments after the function, prints a TqdmDeprecationWarning and calls your function without them, which usually raises a TypeError.

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