pandas 3.0 breaking changes, measured against pandas 2.3
The pandas 3.0 changes that break existing code, each shown by running the same script on pandas 2.3.3 and 3.1.0: the new str dtype, Copy-on-Write (chained assignment silently does nothing), datetime64[us], frequency aliases like 'H' raising, and removed methods such as applymap and fillna(method=).
1. Strings are a new "str" dtype, not object
import pandas as pd
s = pd.Series(["a", "b", None])
print(s.dtype, repr(s[2]))
df = pd.DataFrame({"x": ["1", "2"]})
print(df.dtypes.to_dict())
print(df.select_dtypes(include="object").columns.tolist())
try:
s[0] = 5
print("assign int ok:", s.tolist())
except Exception as e:
print(type(e).__name__ + ":", e)
pandas 2.3.3:
object None
{'x': dtype('O')}
['x']
assign int ok: [5, 'b', None]
pandas 3.1.0:
/w/b1.py:6: Pandas4Warning: For backward compatibility, 'str' dtypes are included by select_dtypes when 'object' dtype is specified. This behavior is deprecated and will be removed in a future version. Explicitly pass 'str' to `include` to select them, or to `exclude` to remove them and silence this warning.
See https://pandas.pydata.org/docs/dev/user_guide/migration.html#string-migration-select-dtypes for details on how to write code that works with pandas 2 and 3.
print(df.select_dtypes(include="object").columns.tolist())
str nan
{'x': <StringDtype(storage='python', na_value=nan)>}
['x']
TypeError: Invalid value '5' for dtype 'str'. Value should be a string or missing value, got 'int' instead.
Text columns now get dtype: str (a StringDtype with NaN as the missing value), so a None in a string column reads back as nan, and putting an int into a string column raises TypeError: Invalid value '5' for dtype 'str' instead of silently making a mixed object column. select_dtypes(include="object") still picks up str columns but emits a Pandas4Warning; pass "str" explicitly. Error messages change too: KeyErrors now print dtype='str' (see KeyError: not in index).
2. Copy-on-Write: chained assignment silently does nothing
import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3], "b": [10, 20, 30]})
sub = df[df["a"] > 1]
sub["b"] = 0
print(df["b"].tolist())
s = df["a"]
s[0] = 99
print(df["a"].tolist())
pandas 2.3.3:
/w/b2.py:4: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
sub["b"] = 0
[10, 20, 30]
[99, 2, 3]
pandas 3.1.0:
[10, 20, 30]
[1, 2, 3]
Under Copy-on-Write every indexing result behaves like a copy. The SettingWithCopyWarning is gone, and so is the old behaviour where s = df["a"]; s[0] = 99 also changed df: on 3.1.0 df stays [1, 2, 3] with no message at all. Write through the frame instead: df.loc[df["a"] > 1, "b"] = 0. Details: SettingWithCopyWarning in pandas 2 vs 3.
3. Datetimes default to microseconds; old frequency aliases are errors
import pandas as pd
t = pd.to_datetime(["2026-10-23 01:02:03"])
print(t.dtype)
print(pd.Timestamp("2026-10-23").unit)
for alias in ["H", "T", "S", "M", "ME", "Y", "YE", "h"]:
try:
r = pd.date_range("2026-01-01", periods=2, freq=alias)
print(alias, "ok", r.freqstr)
except Exception as e:
print(alias, type(e).__name__ + ":", str(e)[:22])
pandas 2.3.3:
/w/b3.py:7: FutureWarning: 'H' is deprecated and will be removed in a future version, please use 'h' instead.
r = pd.date_range("2026-01-01", periods=2, freq=alias)
/w/b3.py:7: FutureWarning: 'T' is deprecated and will be removed in a future version, please use 'min' instead.
r = pd.date_range("2026-01-01", periods=2, freq=alias)
/w/b3.py:7: FutureWarning: 'S' is deprecated and will be removed in a future version, please use 's' instead.
r = pd.date_range("2026-01-01", periods=2, freq=alias)
/w/b3.py:7: FutureWarning: 'M' is deprecated and will be removed in a future version, please use 'ME' instead.
r = pd.date_range("2026-01-01", periods=2, freq=alias)
/w/b3.py:7: FutureWarning: 'Y' is deprecated and will be removed in a future version, please use 'YE' instead.
r = pd.date_range("2026-01-01", periods=2, freq=alias)
datetime64[ns]
s
H ok h
T ok min
S ok s
M ok ME
ME ok ME
Y ok YE-DEC
YE ok YE-DEC
h ok h
pandas 3.1.0:
datetime64[us]
us
H ValueError: Invalid frequency: H.
T ValueError: Invalid frequency: T.
S ValueError: Invalid frequency: S.
M ValueError: Invalid frequency: M.
ME ok ME
Y ValueError: Invalid frequency: Y.
YE ok YE-DEC
h ok h
Parsed strings now give datetime64[us] instead of datetime64[ns], which matters if you convert to integers (.astype("int64") is now microseconds since the epoch) or compare dtypes. The aliases H, T, S, M and Y that warned in 2.x now raise ValueError: Invalid frequency; use h, min, s, ME and YE. errors="ignore" in to_datetime is also gone: to_datetime errors='ignore'.
4. Deprecated methods and arguments now fail
import warnings
import pandas as pd
warnings.simplefilter("always")
df = pd.DataFrame({"a": [1.0, None]})
for name, f in [("inplace on column", lambda: df["a"].fillna(0, inplace=True)),
("swapaxes", lambda: df.swapaxes(0, 1)),
("applymap", lambda: df.applymap(str)),
("DataFrame.append", lambda: df.append(df)),
("fillna(method=)", lambda: df.fillna(method="ffill")),
("groupby axis=1", lambda: df.groupby([0], axis=1))]:
with warnings.catch_warnings(record=True) as w:
try:
f()
msg = (type(w[0].message).__name__ + ": " + str(w[0].message)[:60]) if w else "ok, no warning"
except Exception as e:
msg = type(e).__name__ + ": " + str(e)[:60]
print(f"{name}: {msg}")
print("after inplace:", df["a"].tolist())
pandas 2.3.3:
inplace on column: ok, no warning
swapaxes: FutureWarning: 'DataFrame.swapaxes' is deprecated and will be removed in a
applymap: FutureWarning: DataFrame.applymap has been deprecated. Use DataFrame.map in
DataFrame.append: AttributeError: 'DataFrame' object has no attribute 'append'
fillna(method=): FutureWarning: DataFrame.fillna with 'method' is deprecated and will raise
groupby axis=1: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.gro
after inplace: [1.0, 0.0]
pandas 3.1.0:
inplace on column: ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series thro
swapaxes: AttributeError: 'DataFrame' object has no attribute 'swapaxes'
applymap: AttributeError: 'DataFrame' object has no attribute 'applymap'
DataFrame.append: AttributeError: 'DataFrame' object has no attribute 'append'
fillna(method=): TypeError: NDFrame.fillna() got an unexpected keyword argument 'method'
groupby axis=1: TypeError: DataFrame.groupby() got an unexpected keyword argument 'axis
after inplace: [1.0, nan]
Everything that raised a FutureWarning on 2.3.3 is an AttributeError or TypeError on 3.1.0: applymap (use DataFrame.map), swapaxes (use .T), fillna(method=...) (use .ffill()/.bfill()), groupby(axis=1) (transpose first). DataFrame.append was already removed in 2.0; use pd.concat. Note the first line: df["a"].fillna(0, inplace=True) worked silently on 2.3.3 but on 3.1.0 emits ChainedAssignmentError (a Warning subclass, not an exception) and leaves the NaN in place. Use df["a"] = df["a"].fillna(0).
5. Changes with no error at all
These are the dangerous ones, each measured in its own guide:
- groupby:
applydrops the grouping columns andobserved=Trueis the default, so empty categories vanish from results. - concat with empty or all-NA frames: the result dtype can change from float64 to object.
- fillna/replace downcasting: no longer downcasts object columns; the opt-in option is itself deprecated.
- warning filters:
Pandas4Warningis aDeprecationWarningsubclass, so filters written for 2.x FutureWarnings may not match. - read_excel: the missing-dependency message changed and
.xlsnow wants python-calamine.
How to upgrade safely
Upgrade to pandas 2.3.x first and run your code with warnings visible (python -W always::FutureWarning): almost every hard error in sections 3 and 4 shows up there as a FutureWarning first. The silent changes in sections 2 and 5 don't warn, so check outputs, not just exit codes. If numpy 2 is also part of the upgrade, see _ARRAY_API not found / numpy 2 mismatch.
More pandas errors: KeyError on a column that exists · merge KeyError · unhashable type: 'dict' · MemoryError: unable to allocate · to_numpy() read-only: assignment destination is read-only.
Frequently asked questions
What are the biggest breaking changes in pandas 3.0?
String columns default to the new str dtype, Copy-on-Write is always on (chained assignment no longer modifies the original), datetimes parse to microsecond resolution, and methods deprecated in 2.x such as applymap, swapaxes and fillna(method=) are removed.
Why does my chained assignment not work in pandas 3?
Under Copy-on-Write, df[mask]['col'] = x and s = df['col']; s[0] = x modify a copy. Use df.loc[mask, 'col'] = x. In our test, pandas 3.1.0 gave no warning for the s[0] case.
How do I prepare code for pandas 3?
Upgrade to pandas 2.3 first and fix every FutureWarning; most of the hard errors in 3.0 are warnings there. Silent changes (groupby, concat dtypes, Copy-on-Write) need output checks.
Written with AI assistance. Measured on 2026-10-23 in the python:3-alpine Docker image (Python 3.14.7) with pandas 2.3.3 and pandas 3.1.0, same script on both. Outputs pasted unedited. PlainTables is our extension; Python and pandas are free.