Skip to content

Implement construct_from_string on PandasArrayExtensionDtype - #8468

Open
shashvat-singham wants to merge 1 commit into
huggingface:mainfrom
shashvat-singham:fix-pandas-array-extension-dtype-construct-from-string
Open

Implement construct_from_string on PandasArrayExtensionDtype#8468
shashvat-singham wants to merge 1 commit into
huggingface:mainfrom
shashvat-singham:fix-pandas-array-extension-dtype-construct-from-string

Conversation

@shashvat-singham

Copy link
Copy Markdown

Fixes #8467.

The bug

PandasArrayExtensionDtype never implements construct_from_string, so it inherits the base ExtensionDtype version:

@classmethod
def construct_from_string(cls, string):
    assert isinstance(cls.name, str), (cls, type(cls.name))

name is an instance @property here, because it depends on value_type (array[float64]). On the class it is a property object, so the assertion fires:

AssertionError: (<class 'datasets.features.features.PandasArrayExtensionDtype'>, <class 'property'>)

ExtensionDtype.__eq__ routes every dtype == "some string" through construct_from_string, and pandas compares dtypes against strings throughout its internals. On main:

FAIL | dtype == 'string'          | AssertionError: (PandasArrayExtensionDtype, <class 'property'>)
FAIL | is_string_dtype(dtype)     | AssertionError: (PandasArrayExtensionDtype, <class 'property'>)
FAIL | df.astype(object)          | AssertionError: (PandasArrayExtensionDtype, <class 'property'>)
FAIL | df.foo.astype(object)      | AssertionError: (PandasArrayExtensionDtype, <class 'property'>)

df.astype(object) gets there via astype_is_viewis_string_dtypedtype == "string".

The fix

Implement construct_from_string: parse array[<dtype>] back into a PandasArrayExtensionDtype, and raise TypeError for anything else — which is how pandas spells "not my dtype", and what __eq__ catches to return False.

Tests

In tests/features/test_array_xd.py:

  • test_pandas_array_extension_dtype_construct_from_stringarray[int32] / array[bool] / array[float64] round-trip to the right value_type.
  • test_pandas_array_extension_dtype_construct_from_string_rejectsTypeError for string, int64, array[], array[not_a_dtype], not_an_array[int32].
  • test_table_to_pandas_dtype_compares_to_string — the user-facing symptom: dtype == "string" is False, is_string_dtype is False, df.astype(object) works.

11 of these fail on main and pass here; full tests/features/test_array_xd.py is green (114 passed). ruff check / ruff format --check clean.

Interaction with #8464

Independent fixes, but they compound, so flagging it rather than letting CI surprise you. With only this change, pandas gets further and then trips the _metadata bug from #8375:

ok   | dtype == 'string'                            | False
FAIL | dtype == 'array[float64]'                    | AttributeError: ... has no attribute 'v'
FAIL | df.convert_dtypes()                          | AttributeError: ... has no attribute 'v'
FAIL | df.melt()                                    | AttributeError: ... has no attribute 'v'
FAIL | pd.concat([df, df.astype({'n':'float64'})])  | AttributeError: ... has no attribute 'v'

With both this and #8464 applied, all of those pass. This PR is based on main and does not include the _metadata change, so the two can be reviewed and merged in either order — the tests here are written to not depend on #8464.

The one thing still failing with both applied is df.replace(1.0, 2.0), which hits the deliberate NotImplementedError("Invalid type to compare to: <class 'float'>") in PandasArrayExtensionArray.__eq__. That looks intentional, so I left it alone.

Tested on Windows 11 / Python 3.11.9 / pandas 3.0.5 / pyarrow 25.0.1 / numpy 2.4.6.

Pandas compares dtypes against plain strings throughout its internals, and
`ExtensionDtype.__eq__` routes those through `construct_from_string`. The
base implementation starts with

    assert isinstance(cls.name, str), (cls, type(cls.name))

`name` is a property here because it depends on `value_type`, so the
assertion fires and every such comparison raised

    AssertionError: (PandasArrayExtensionDtype, <class 'property'>)

That took out `df.astype(object)`, `is_string_dtype(dtype)` and any bare
`dtype == "some string"`.

Parse "array[<dtype>]" back into the dtype, and raise TypeError for
anything else, which is how pandas spells "not my dtype".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PandasArrayExtensionDtype has no construct_from_string, so comparing the dtype to any string raises AssertionError

1 participant