pandas 3: why do blank cells now crash .lower() with “‘float’ object has no attribute ‘lower’”?

Blank cells load into pandas as NaN, which is a float, and a float has no .lower() method. If your code calls .lower() on each value one at a time, such as with apply or map, it fails on the first blank. The fix is to use the vectorized .str.lower(), which skips missing values.

I reproduced this on pandas 3.0.5. Note that read_csv has long turned blank cells into NaN, so this error is not new in itself. What pandas 3 changes is the dtype and how missing values are stored, which can make code that used to work fail.

What pandas 3 changed

In pandas 3, a column of strings gets a new default dtype, shown as str. The pandas docs say its missing-value marker is always NaN. If you build a column with None in it, pandas 3 turns the None into NaN, whereas the old object dtype kept None. So anything that depended on None, or on the column being object dtype, can behave differently.

A small example

This data has one blank name:

import io
import pandas as pd

df = pd.read_csv(io.StringIO("name,n\nAlice,1\n,2\nBob,3\n"))
df["name"].dtype                     # str
df["name"].apply(lambda x: x.lower())
# AttributeError: 'float' object has no attribute 'lower'

The lambda receives the blank cell as NaN, so x.lower() fails.

Fixes that work

  1. Use the .str accessor. df["name"].str.lower() returns alice, NaN, bob. The pandas docs say string methods exclude missing values automatically, so they pass through instead of raising errors.
  2. Guard the lambda. df["name"].apply(lambda x: x.lower() if isinstance(x, str) else x) keeps the blank as NaN.
  3. Skip missing values in map. df["name"].map(str.lower, na_action="ignore") does the same and reads cleanly. The pandas migration guide uses na_action=”ignore” for the same reason with map(str).
  4. Fill first. df["name"].fillna("").str.lower() turns blanks into empty strings, which is useful when later code expects text everywhere.

I ran all four on pandas 3.0.5 and each one completed without error.

What does not fix it

Casting to object does not help. Calling .astype(object).apply(lambda x: x.lower()) still fails, because the blank is still a float NaN. Also avoid testing for blanks with x is None or x == np.nan. The migration guide recommends pd.isna(), and says to pass it a single value inside an if condition.

Find the blanks before they bite

To see which columns contain missing values, run df.isna().sum(). It counts the blanks in each column, so you know where a per-value method like .lower() could fail. If a column should never be blank, it is better to find out why it is, rather than silently filling it.

When you do want to treat blanks as empty text, fill them once, right after loading the data, and then every later step can assume it is working with strings. When blanks carry meaning, such as “not entered yet”, leave them as NaN and let the .str methods pass them through.

Other things that may break

  • Dtype checks: ser.dtype == "object" is False for string columns on pandas 3. Use pd.api.types.is_string_dtype(ser.dtype) to work on both versions.
  • Assigning a non-string, such as a number, into a str column raises a TypeError. Convert the column to object first if you need mixed values.
  • You can test your code before upgrading. The migration guide says pandas 2.3 offers the new behaviour as an opt-in: pd.options.future.infer_string = True.

Sources

Last checked: 18 October 2026