What's New in pandas 3.0
pandas 3.0 was released on 21 January 2026. It is the biggest change to pandas in years, and it affects everyday code: how text columns are stored, what happens when you change a selection, how dates are stored, and which old methods still exist. This course already teaches the pandas 3.0 way, but you will meet a lot of pandas 1 and 2 code in tutorials, answers and company projects.
This lesson collects the changes in one place, with the old way and the new way side by side, and ends with a checklist for upgrading old code.
At a glance
| Area | pandas 2.x | pandas 3.0 |
|---|---|---|
| Text columns | dtype object | dedicated string dtype, shown as str |
| Changing a selection | sometimes changed the original, SettingWithCopyWarning | Copy-on-Write: selections always behave as copies |
df["a"][0] = 1 (chained assignment) | sometimes worked | never works, warns ChainedAssignmentError |
| Parsed dates | datetime64[ns] | datetime64[us] for most inputs |
| Column expressions | lambda df: df["a"] + 1 | pd.col("a") + 1 also available |
| Setting a float into an int column | warning, column upcast | TypeError |
groupby on categories | showed unused categories by default | observed=True is the default |
| Requirements | older Python and NumPy versions; pytz required | Python 3.11+, NumPy 1.26+; pytz optional |
The new string dtype
In pandas 3.0, a column of text gets a dedicated string dtype, displayed as str. Missing values in it are NaN, and it accepts only strings or missing values. If the pyarrow package is installed, pandas uses it to store the text compactly and process it faster; otherwise it falls back to Python storage (as in the browser editor). Your code is the same either way.
import pandas as pd
cities = pd.Series(["Pune", "Kochi", None])
print(cities.dtype, "| storage:", cities.dtype.storage)
print(cities.dtype == "str", cities.dtype == object)
try:
cities[0] = 411001 # a number into a text column
except TypeError as err:
print("TypeError:", err)
sales = pd.read_csv("sales.csv")
print(sales.select_dtypes(include="str").columns.tolist())
old_style = pd.Series(["Pune", 411001], dtype=object)
print(old_style.dtype)str | storage: python True False TypeError: Invalid value '411001' for dtype 'str'. Value should be a string or missing value, got 'int' instead. ['order_id', 'order_date', 'city', 'state', 'category', 'product', 'payment_mode'] object
Code that checks dtype == object to find text columns now gets False; use select_dtypes(include="str") or pd.api.types.is_string_dtype(). Putting a number into a text column fails instead of silently mixing types. When you genuinely need mixed Python objects in one column, ask for dtype=object explicitly.
Copy-on-Write
Copy-on-Write (CoW) is now the only behaviour: any result of indexing or of a method behaves like a new object. Changing it never changes the original. pandas avoids the cost of real copies by sharing memory until one side is modified, which is where the name comes from.
import warnings
import pandas as pd
df = pd.DataFrame({"name": ["Aarav", "Priya", "Kabir"], "marks": [71, 88, 64]})
top = df[df["marks"] > 70]
top["marks"] = 100 # changes only top
print(df["marks"].tolist(), top["marks"].tolist())
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
df["marks"][0] = 99 # chained assignment: never works in pandas 3.0
print(caught[0].category.__name__, "| marks still:", df["marks"].tolist())
df.loc[0, "marks"] = 99 # one step on the original: works
print(df["marks"].tolist())[71, 88, 64] [100, 100] ChainedAssignmentError | marks still: [71, 88, 64] [99, 88, 64]
df["marks"][0] = 99 is chained assignment: the first brackets create an intermediate object and the second set a value on it. Under CoW that intermediate object is always a copy, so the original never changes and pandas warns with ChainedAssignmentError. The fix is always the same: do it in one .loc (or .iloc) step on the original.
Dates in microseconds
pandas 3.0 infers the resolution of datetimes from the input. Text dates and Python datetime objects now become datetime64[us] (microseconds) instead of always datetime64[ns] (nanoseconds). Values and calculations look the same; what changes is the range of dates that fits (far wider now) and the integers you get if you convert dates to numbers.
import pandas as pd
d = pd.to_datetime(pd.Series(["2025-10-20", "2025-11-05"]))
print(d.dtype)
print(d.astype("int64").tolist()) # microseconds since 1970
print(d.dt.as_unit("ns").astype("int64").tolist())
print(pd.Timestamp("2500-01-01")) # beyond the old nanosecond limit (year 2262)datetime64[us] [1760918400000000, 1762300800000000] [1760918400000000000, 1762300800000000000] 2500-01-01 00:00:00
If old code turned dates into integers (for example to feed a model), the numbers are now 1,000 times smaller. Convert with .dt.as_unit("ns") first if the old numbers are needed.
pd.col expressions
pd.col("name") refers to a column inside assign() and loc[], without a lambda. It is especially handy in method chains, where the column you need was created in an earlier step.
import pandas as pd
sales = pd.read_csv("sales.csv").drop_duplicates()
old = sales.assign(value=lambda d: d["quantity"] * d["unit_price"]) # pandas 2 style
new = sales.assign(value=pd.col("quantity") * pd.col("unit_price")) # pandas 3 style
print(old["value"].equals(new["value"]))
big = new.loc[pd.col("value") >= 5000, ["product", "quantity", "value"]]
print(big.sort_values("value", ascending=False).head(3))True
product quantity value
1020 Exam Guide 120.0 54000.0
719 Smartwatch 4.0 11996.0
1068 Smartwatch 4.0 11996.0Both styles give identical results, and the lambda style still works, so you do not need to rewrite old code. Use whichever reads more clearly.
Stricter setting and other changes
Setting a value that does not fit a column's dtype used to upcast the column with a warning. pandas 3.0 raises an error instead, so types never change behind your back.
import pandas as pd
qty = pd.Series([1, 2, 3])
try:
qty.iloc[0] = 2.5
except TypeError as err:
print("TypeError:", err)
qty = qty.astype("float64") # change the type deliberately first
qty.iloc[0] = 2.5
print(qty.tolist())
s = pd.Series([78, 91], index=["Aarav", "Priya"])
try:
s[0]
except KeyError:
print("s[0] is a label lookup now; use s.iloc[0] ->", s.iloc[0])TypeError: Invalid value '2.5' for dtype 'int64' [2.5, 2.0, 3.0] s[0] is a label lookup now; use s.iloc[0] -> 78
The second part shows another completed change: with a text index, s[0] no longer falls back to "position 0". Square brackets on a Series use labels; .iloc is for positions.
Removed features and their replacements
| Old code | What to write in pandas 3.0 |
|---|---|
df.applymap(func) | df.map(func) |
df.fillna(method="ffill") | df.ffill() (and df.bfill()) |
resample("M"), "Q", "Y", "H" | "ME", "QE", "YE", "h" |
pd.read_json('[{"a": 1}]') (a JSON string) | pd.read_json(io.StringIO(text)) |
s[0] for the first value of a labelled Series | s.iloc[0] |
df.append(other) (removed in 2.0) | pd.concat([df, other]) |
if df[col].dtype == object: to find text | df.select_dtypes(include="str") |
pandas 3.0 also added anti joins (merge(..., how="left_anti")), new warning classes (Pandas4Warning for things that will change in pandas 4.0), and returns zoneinfo time zones instead of pytz ones.
How to upgrade old code
The pandas team recommends a two-step path:
- Upgrade to pandas 2.3 first and run your code and tests. It warns about most things that change in 3.0. Fix every warning.
- Then upgrade to 3.0, run everything again, and search the code for
dtype == object,applymap,method=, chained][assignments and the old frequency aliases.
On your computer, check the version with pd.__version__ and upgrade with uv add "pandas>=3" or pip install --upgrade pandas.
Common mistakes
- Expecting a filtered DataFrame to update the original. Under CoW it never does; assign with
.locon the original. - Testing
dtype == objectto find text. It isstrnow. - Following old tutorials blindly. Code using
applymap,fillna(method=...)orresample("M")fails in pandas 3.0. - Jumping straight from pandas 1.x to 3.0. Go through 2.3 and fix its warnings first.
Key points
- pandas 3.0 (21 January 2026) changed defaults that affect everyday code.
- Text columns use the
strdtype withNaNfor missing values and accept only strings. - Copy-on-Write: selections behave as copies, chained assignment never works, and
SettingWithCopyWarningis gone. - Parsed dates are
datetime64[us];pd.col()offers lambda-free column expressions. - Incompatible values raise
TypeError;applymap,fillna(method=)and the"M"/"H"aliases were removed.
Exercise
The following pandas 2 code no longer works correctly in pandas 3.0. Rewrite it in the pandas 3.0 style and run it: df = pd.read_csv("weather.csv"), then df["humidity"][df["humidity"].isna()] = 0, then monthly = df.set_index(pd.to_datetime(df["date"]))["humidity"].resample("M").mean(), then df2 = df[["city", "humidity"]].applymap(str). (Also ask yourself: is filling humidity with 0 a good idea?)
Show solution
import pandas as pd
df = pd.read_csv("weather.csv")
df.loc[df["humidity"].isna(), "humidity"] = df["humidity"].median()
monthly = df.set_index(pd.to_datetime(df["date"]))["humidity"].resample("ME").mean()
df2 = df[["city", "humidity"]].map(str)
print(monthly.round(1).head(3))
print(df2.dtypes)date 2025-01-31 66.4 2025-02-28 64.6 2025-03-31 60.4 Freq: ME, Name: humidity, dtype: float64 city str humidity str dtype: object
The chained assignment becomes one .loc step, "M" becomes "ME", and applymap becomes map. Filling humidity with 0 would be wrong, since 0% humidity never happens in these cities, so the median is used instead.
Quick Quiz
1. In pandas 3.0, what dtype does pd.Series(["Pune", "Kochi"]) have?
Text now uses the dedicated string dtype, shown as str.
2. What happens with df["marks"][0] = 99 in pandas 3.0?
Under Copy-on-Write, chained assignment always modifies a temporary copy.
3. Which replaces df.fillna(method="ffill")?
The method argument was removed; use ffill() or bfill().
4. What is the recommended path for upgrading old code to pandas 3.0?
pandas 2.3 warns about most behaviour that changes in 3.0.
Learn Data Science with a mentor
Free lessons get you started. Our Data Science & Machine Learning training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
