Fix
Getting Started With Python Excel AutomationDeep dive

Fix pandas FutureWarnings in Excel Pipelines

Clear the FutureWarnings an Excel reporting script prints under pandas 2.x before they turn into errors in pandas 3: month-end and hourly frequency aliases, fillna(method=), applymap, positional Series indexing, chained assignment and inplace on a column — plus the silent pandas 3 change to text columns that breaks dtype == object checks — with each warning reproduced under both versions and fixed.

A reporting script that has run quietly for years starts printing FutureWarning messages after a pandas upgrade. They are easy to ignore — the report still comes out right — and that is exactly the danger: a FutureWarning is pandas announcing that the code will stop working in a later release. With pandas 3, many of the warnings that pandas 2.2 printed have become errors, and one change produces no warning at all and simply changes behaviour. This page reproduces the warnings that turn up most in Excel pipelines — resampling by month, hourly frequencies, filling gaps, formatting every cell, positional indexing, chained assignment and in-place updates of a column — runs them under pandas 2.2 and pandas 3 side by side, and gives the replacement for each that works in both. It belongs to Troubleshooting Common Python Excel Errors.

Prerequisites

Bash
pip install pandas openpyxl     # and uv, to run the probe under two pandas versions

The comparison runs a small probe script in two throwaway environments created by uv, one with pandas 2.2.3 and one with pandas 3.0.6, so the behaviour of both versions is shown from real runs on the same machine. Testing a whole project against several versions is covered in Test Excel Code Against Several Library Versions with nox.

Step 1 — The old patterns, under both versions

Python
import warnings
import pandas as pd

df = pd.DataFrame({"region": ["N", "S", "N"], "amount": [1.0, None, 3.0],
                   "date": pd.to_datetime(["2026-01-05", "2026-02-03", "2026-03-09"])})
probes = {
    "resample('M')": lambda: df.set_index("date").resample("M")["amount"].sum(),
    "freq='H'": lambda: pd.date_range("2026-01-01", periods=3, freq="H"),
    "fillna(method=)": lambda: df.fillna(method="ffill"),
    "applymap": lambda: df.applymap(str),
    "s[0] on labels": lambda: pd.Series([1, 2], index=["a", "b"])[0],
    "chained assignment": lambda: df["amount"].__setitem__(df["region"] == "N", 0),
    "inplace on a column": lambda: df["amount"].fillna(0, inplace=True),
    "select_dtypes('object')": lambda: df.select_dtypes("object"),
    "dtype == object": lambda: df["region"].dtype == object,
}
print("pandas", pd.__version__)
for name, probe in probes.items():
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always")
        try:
            result = probe()
            msg = caught[0].category.__name__ if caught else ("returns " + str(result) if isinstance(result, bool) else "ok")
        except Exception as e:
            msg = type(e).__name__
    print(f"  {name:<24} {msg}")
Python
import subprocess

for version in ["2.2.3", "3.0.6"]:
    out = subprocess.run(["uv", "run", "--quiet", "--no-project", "--python", "3.13", "--with", f"pandas=={version}",
                          "python", "probe.py"], capture_output=True, text=True)
    print(out.stdout.strip())

Under pandas 2.2.3, every one of the first seven patterns works but prints a FutureWarning. Under pandas 3.0.6 the first five are errors — ValueError, TypeError, AttributeError, KeyError — and the two assignment patterns raise a ChainedAssignmentError warning because, with copy-on-write now always on, they no longer change the DataFrame at all. The last two lines are the quiet ones: select_dtypes("object") gains a new deprecation warning, and dtype == object on a text column changes from True to False with no warning whatsoever.

The same code under pandas 2.2 and pandas 3 Month-end and hourly aliases, fillna with method, applymap and positional indexing with square brackets warn in 2.2 and fail in 3. Chained assignment and inplace on a column warn in 2.2 and do nothing in 3. select_dtypes object starts warning in 3, and dtype == object changes from True to False silently. pandas 2.2 pandas 3 use instead resample('M') FutureWarning ValueError 'ME' freq='H' FutureWarning ValueError 'h' fillna(method='ffill') FutureWarning TypeError .ffill() applymap FutureWarning AttributeError .map() s[0] on labels FutureWarning KeyError .iloc[0] chained / inplace on column FutureWarning no effect df.loc[...] = dtype == object True False is_string_dtype

Step 2 — The replacements

Each pattern has a replacement that works under both versions:

Python
import pandas as pd
from pandas.api.types import is_string_dtype

df = pd.DataFrame({"region": ["N", "S", "N"], "amount": [1.0, None, 3.0],
                   "date": pd.to_datetime(["2026-01-05", "2026-02-03", "2026-03-09"])})

monthly = df.set_index("date").resample("ME")["amount"].sum()        # 'M' -> 'ME' (month end)
hours = pd.date_range("2026-01-01", periods=3, freq="h")              # 'H' -> 'h'
filled = df.ffill()                                                   # fillna(method="ffill") -> ffill()
as_text = df.map(str)                                                 # applymap -> map
first = pd.Series([1, 2], index=["a", "b"]).iloc[0]                   # s[0] -> s.iloc[0]
df.loc[df["region"] == "N", "amount"] = 0                             # df["amount"][mask] = 0 -> df.loc[mask, col]
df["amount"] = df["amount"].fillna(0)                                 # inplace on a column -> assign the result
text_cols = [c for c in df.columns if is_string_dtype(df[c])]         # dtype == object -> is_string_dtype
print(monthly.index.strftime("%Y-%m-%d").tolist(), "|", df["amount"].tolist(), "|", text_cols)

The frequency aliases were renamed to say what they mean: ME is month end (as opposed to MS, month start), and the time units became lower case — h, min, s. ffill() and bfill() replace the method= argument. DataFrame.map is the element-wise function that used to be called applymap. Square brackets on a Series now always mean labels, so position needs iloc. And the two assignment forms are replaced by a single loc assignment on the DataFrame, or by assigning the result of a method back to the column — the only forms that change the data under copy-on-write. All of these replacements already exist in pandas 2.2, so the code can be fixed before upgrading and will run unchanged on both versions — there is no need to support two code paths. These are mechanical edits; a search for resample("M", freq="H", method=, applymap and inplace=True finds most of them in an Excel pipeline.

Clearing the warnings in a project Run the pipeline with warnings shown, or the test suite with warnings as errors. Search for the known patterns, apply the mechanical replacements, check text-column logic that compared dtype with object, and rerun under the newest pandas until no warnings remain. Surface warnings -W error in tests Search patterns M, H, method=, applymap Replace ME, h, ffill, map, loc Check text columns dtype == object Rerun on pandas 3 no warnings left

Why the assignment patterns stop working

The two assignment patterns deserve a closer look, because in pandas 3 they fail by doing nothing. df["amount"][mask] = 0 first takes the column df["amount"] — under copy-on-write, a new Series that only shares memory with the DataFrame until it is modified — and then sets values on that Series, which triggers a copy; the DataFrame is untouched. df["amount"].fillna(0, inplace=True) does the same thing with a method. In pandas 2 these sometimes worked and sometimes did not, depending on internal details, which is why pandas warned; in pandas 3 they never work, and the warning is the only sign. Excel scripts are full of these patterns, often in the cleaning step just before to_excel, so the symptom is a report that still contains the blanks or bad values the code was supposed to fix.

Chained assignment or a single loc assignment Chained assignment first takes a column and then sets values on it, which in pandas 3 modifies a copy and leaves the DataFrame unchanged. A single loc assignment, or assigning a method's result back to the column, changes the DataFrame in every version. df['amount'][mask] = 0 takes the column first sets values on a copy DataFrame unchanged in pandas 3 ChainedAssignmentError warning df.loc[mask, 'amount'] = 0 one operation on the DataFrame changes the data same in pandas 2 and 3 no warning

Making warnings visible in scheduled jobs

Warnings go to standard error, which a scheduled job often discards, so nobody sees them until the upgrade that turns them into errors. Two settings fix that. In the job itself, logging.captureWarnings(True) routes every warning into the logging system, so FutureWarnings land in the same log file as the rest of the run and can be searched for. In the test suite, run pytest with -W error::FutureWarning — or set filterwarnings = ["error"] in the project configuration — so a deprecation fails a test the day it appears rather than the day the code breaks. Setting the environment variable PYTHONWARNINGS=error::FutureWarning does the same for a one-off run of a script. Logging in general is covered in Log Python Excel Script Output to a File.

Step 3 — The silent one: text columns are no longer object

In pandas 3, text read from Excel is stored in the new str dtype rather than object. Code that finds text columns by comparing with object stops finding them, without any warning:

Python
from openpyxl import Workbook

wb = Workbook()
wb.active.append(["Customer", "City", "Amount"])
wb.active.append(["  Acme Ltd ", " Leeds", 120.5])
wb.save("customers.xlsx")
customers = pd.read_excel("customers.xlsx")

trimmed_old = [c for c in customers.columns if customers[c].dtype == object]
trimmed_new = [c for c in customers.columns if is_string_dtype(customers[c])]
print(pd.__version__, "| dtype == object finds:", trimmed_old, "| is_string_dtype finds:", trimmed_new)

Under pandas 3 the old test finds no text columns, so a cleaning step that trims spaces from "every text column" quietly trims nothing and the report keeps " Acme Ltd ". is_string_dtype finds both columns under either version. The same applies to select_dtypes: use include=["object", "string"] rather than "object" alone. Of all the changes on this page, this is the one most worth searching for, precisely because it does not announce itself. Related checks deserve the same review: isinstance(value, str) still works on individual values, but code that inspects df.dtypes to decide how to format columns when writing to Excel — text left-aligned, numbers with a number format — should also use is_string_dtype and is_numeric_dtype rather than comparing dtype names. Missing text values change too: in the new string dtype a missing value is NaN, so tests that compared with None need pd.isna().

Common pitfalls

  • Ignoring FutureWarnings. They are the list of what will break on the next upgrade.
  • Silencing warnings globally. warnings.filterwarnings("ignore") hides the useful ones too.
  • Replacing 'M' with 'MS'. That moves results to month start; month end is 'ME'.
  • Fixing chained assignment with .copy(). Assign through df.loc instead.
  • Checking dtype == object for text. Use is_string_dtype.

Conclusion

The FutureWarnings pandas 2.2 prints in Excel pipelines are the errors of pandas 3: replace resample("M") with "ME", freq="H" with "h", fillna(method=...) with ffill() or bfill(), applymap with map, s[0] with s.iloc[0], and chained or in-place column updates with df.loc[...] = or an assignment of the result. Then look for text-column logic based on dtype == object, which pandas 3 changes silently, and use is_string_dtype instead.

Frequently asked questions

Why does resample("M") fail in pandas 3? The month-end alias was renamed to "ME"; "M" warned in pandas 2.2 and raises ValueError in pandas 3.

What replaces fillna(method="ffill")? Call df.ffill() or df.bfill() directly; the method argument was removed in pandas 3.

Why does my assignment df["col"]mask = value no longer change the DataFrame? With copy-on-write in pandas 3, chained assignment works on a copy; use df.locmask, "col" = value.

Why are my text columns not detected as object dtype in pandas 3? pandas 3 stores text in the new str dtype; test with pandas.api.types.is_string_dtype instead of comparing with object.

More on errors & fixes

More on errors & fixes →