What's New in pandas 3.0

Data Science · Lesson 30 of 32 7 min read Updated

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

Areapandas 2.xpandas 3.0
Text columnsdtype objectdedicated string dtype, shown as str
Changing a selectionsometimes changed the original, SettingWithCopyWarningCopy-on-Write: selections always behave as copies
df["a"][0] = 1 (chained assignment)sometimes workednever works, warns ChainedAssignmentError
Parsed datesdatetime64[ns]datetime64[us] for most inputs
Column expressionslambda df: df["a"] + 1pd.col("a") + 1 also available
Setting a float into an int columnwarning, column upcastTypeError
groupby on categoriesshowed unused categories by defaultobserved=True is the default
Requirementsolder Python and NumPy versions; pytz requiredPython 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.

PythonRun it yourself
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)
Output
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.

PythonRun it yourself
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())
Output
[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.

PythonRun it yourself
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)
Output
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.

PythonRun it yourself
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))
Output
True
         product  quantity    value
1020  Exam Guide     120.0  54000.0
719   Smartwatch       4.0  11996.0
1068  Smartwatch       4.0  11996.0

Both 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.

PythonRun it yourself
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])
Output
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 codeWhat 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 Seriess.iloc[0]
df.append(other) (removed in 2.0)pd.concat([df, other])
if df[col].dtype == object: to find textdf.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:

  1. 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.
  2. 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 .loc on the original.
  • Testing dtype == object to find text. It is str now.
  • Following old tutorials blindly. Code using applymap, fillna(method=...) or resample("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 str dtype with NaN for missing values and accept only strings.
  • Copy-on-Write: selections behave as copies, chained assignment never works, and SettingWithCopyWarning is 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
Python
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)
Output
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?

2. What happens with df["marks"][0] = 99 in pandas 3.0?

3. Which replaces df.fillna(method="ffill")?

4. What is the recommended path for upgrading old code to pandas 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