@@ -63,10 +63,19 @@ static LogicalType BindColumn(ClientContext &context, PandasBindColumn &column_p
6363 D_ASSERT (py::hasattr (column.attr (" cat" ), " categories" ));
6464 NumpyArray categories (column.attr (" cat" ).attr (" categories" ));
6565 auto categories_pd_type = ConvertNumpyType (categories.GetArray ().attr (" dtype" ));
66- if (categories_pd_type.type == NumpyNullableType::OBJECT ) {
66+ // Legacy categories are backed by an `object` dtype; pandas >= 3.0 backs string categories with the new
67+ // StringDtype (reported as "str"), so treat both as string categories -> ENUM.
68+ if (categories_pd_type.type == NumpyNullableType::OBJECT ||
69+ categories_pd_type.type == NumpyNullableType::STRING ) {
6770 // Let's hope the object type is a string.
6871 bind_data.numpy_type .type = NumpyNullableType::CATEGORY ;
69- vector<string> enum_entries = py::cast<vector<string>>(categories.GetArray ());
72+ // str()-ify each category individually: pandas >= 3.0 categories are a StringArray whose elements are
73+ // numpy str scalars, which nanobind's vector<string>/string casters reject (py::cast<vector<string>>
74+ // on the array throws). Iterating + py::str handles both that and the legacy object[str] case.
75+ vector<string> enum_entries;
76+ for (auto category : categories.GetArray ()) {
77+ enum_entries.push_back (py::cast<std::string>(py::str (category)));
78+ }
7079 idx_t size = enum_entries.size ();
7180 Vector enum_entries_vec (LogicalType::VARCHAR , size);
7281 auto enum_entries_ptr = FlatVector::GetDataMutable<string_t >(enum_entries_vec);
@@ -75,8 +84,11 @@ static LogicalType BindColumn(ClientContext &context, PandasBindColumn &column_p
7584 }
7685 D_ASSERT (py::hasattr (column.attr (" cat" ), " codes" ));
7786 column_type = LogicalType::ENUM (enum_entries_vec, size);
78- NumpyArray pandas_col (column.attr (" cat" ).attr (" codes" ));
79- bind_data.internal_categorical_type = py::cast<std::string>(py::str (pandas_col.GetArray ().attr (" dtype" )));
87+ // .to_numpy(): pandas >= 3.0 returns cat.codes as a Series (no .strides/.ctypes), but the scan needs a
88+ // real ndarray backing buffer; materialize it. (Older pandas returned an ndarray here directly.)
89+ NumpyArray pandas_col (column.attr (" cat" ).attr (" codes" ).attr (" to_numpy" )());
90+ bind_data.internal_categorical_type =
91+ py::cast<std::string>(py::str (py::object (pandas_col.GetArray ().attr (" dtype" ))));
8092 bind_data.pandas_col = std::make_unique<PandasNumpyColumn>(std::move (pandas_col));
8193 } else {
8294 NumpyArray pandas_col (column.attr (" to_numpy" )());
0 commit comments