Fix pandas FutureWarnings in Excel Pipelines
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
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
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}")
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.
Step 2 — The replacements
Each pattern has a replacement that works under both versions:
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.
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.
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:
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 throughdf.locinstead. - Checking
dtype == objectfor text. Useis_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.
Related
- Up one level: Troubleshooting Common Python Excel Errors — fixes for common errors.
- Fix "ExcelWriter Object Has No Attribute save" — an earlier removal.
- Test Excel Code Against Several Library Versions with nox — catching these automatically.
- Strip Whitespace and Normalise Text Columns with pandas — the cleaning step affected by the dtype change.
More on errors & fixes
- FixHandle Permission Denied When Writing Excel in Python
- How-toWhat openpyxl Loses When You Save an Existing Workbook
- FixFix Excel Serial Numbers Showing Instead of Dates
- How-toHandle Daylight Saving Time in Excel Timestamps
- How-toHandle Microsoft Graph Throttling in Excel Automation
- GuideError Handling and Logging in Excel Automation