Skip to content

BUG: read_parquet converts pyarrow list type to numpy dtype #53011

Open

Description

@danielhanchen

Pandas version checks

  • I have checked that this issue has not already been reported.

  • I have confirmed this bug exists on the latest version of pandas.

  • I have confirmed this bug exists on the main branch of pandas.

Reproducible Example

import pandas as pd
import pyarrow as pa
pyarrow_list_of_strings = pd.ArrowDtype(pa.list_(pa.string()))
data = pd.DataFrame({
    "Pyarrow" : pd.Series([["a"], ["a", "b"]], dtype = pyarrow_list_of_strings),
})
data.to_parquet("data.parquet") # SUCCESS
pd.read_parquet("data.parquet") # *** FAIL

data_object = pd.DataFrame({
    "Pyarrow" : pd.Series([["a"], ["a", "b"]], dtype = object),
})
data_object.to_parquet("data.parquet")
pyarrow_internal = pa.parquet.read_table("data.parquet") # SUCCESS with type list[string]
pyarrow_internal .to_pandas() # SUCCESS except object now

pd.Series(pd.arrays.ArrowExtensionArray(pyarrow_internal["Pyarrow"])) # SUCCESS - data-type also correct!

Issue Description

Great work on extending Arrow to Pandas!
Using pd.ArrowDtype(pa.list_(pa.string())) or any other alteration works in the Parquet saving mode, but fails during the reading of the parquet file.

In fact, if there is a Pandas Series of pure lists of strings for eg ["a"], ["a", "b"], Parquet saves it internally as a list[string] type. When Pandas reads the parquet file, it then converts it to an object type.

Is there a way during the reading step to either:

  1. Convert the data-type like in the pure list mode to an object type OR
  2. pd.Series(pd.arrays.ArrowExtensionArray(x)) seems to actually work! Maybe during the conversion from the internal Pyarrow representation into Pandas, we can use pd.Series(pd.arrays.ArrowExtensionArray(x)) on columns which had errors? OR
  3. Somehow support these new types?

Expected Behavior

import pandas as pd
import pyarrow as pa
pyarrow_list_of_strings = pd.ArrowDtype(pa.list_(pa.string()))
data = pd.DataFrame({
    "Pyarrow" : pd.Series([["a"], ["a", "b"]], dtype = pyarrow_list_of_strings),
})
data.to_parquet("data.parquet") # SUCCESS
pd.read_parquet("data.parquet") # SUCCESS

Installed Versions

Details

INSTALLED VERSIONS

commit : 37ea63d
python : 3.11.3.final.0
python-bits : 64
OS : Windows
OS-release : 10
Version : 10.0.19045
machine : AMD64
processor : Intel64 Family 6 Model 30 Stepping 5, GenuineIntel
byteorder : little
LC_ALL : None
LANG : None
LOCALE : English_Australia.1252

pandas : 2.0.1
numpy : 1.24.3
pytz : 2023.3
dateutil : 2.8.2
setuptools : 67.7.2
pip : 23.1.2
Cython : 0.29.34
pytest : None
hypothesis : None
sphinx : None
blosc : None
feather : None
xlsxwriter : None
lxml.etree : None
html5lib : None
pymysql : None
psycopg2 : None
jinja2 : 3.1.2
IPython : 8.12.0
pandas_datareader: None
bs4 : 4.12.2
bottleneck : None
brotli :
fastparquet : None
fsspec : None
gcsfs : None
matplotlib : 3.7.1
numba : 0.57.0rc1
numexpr : None
odfpy : None
openpyxl : 3.1.2
pandas_gbq : None
pyarrow : 11.0.0
pyreadstat : None
pyxlsb : None
s3fs : None
scipy : 1.10.1
snappy : None
sqlalchemy : None
tables : None
tabulate : None
xarray : None
xlrd : 2.0.1
zstandard : 0.21.0
tzdata : 2023.3
qtpy : None
pyqt5 : None

Activity

  1. danielhanchen commented on May 1, 2023

    @danielhanchen
    Author

    I found during the Pyarrow conversion, if you pass in a types_mapper and setting ignore_metadata to False, it works!

    mapping = {schema.type : pd.ArrowDtype(schema.type) for schema in data.schema}
    data.to_pandas(types_mapper = mapping.get, ignore_metadata = True)
    
  2. mroeschke commented on May 1, 2023

    @mroeschke
    Member

    From the traceback, it appears that pyarrow tries to convert this type to a numpy dtype by default, so I think an appropriate fix would be for pyarrow to just return an ArrowDtype here

    File /opt/miniconda3/envs/pandas-dev/lib/python3.10/site-packages/pyarrow/pandas_compat.py:812, in table_to_blockmanager(options, table, categories, ignore_metadata, types_mapper)
        809     table = _add_any_metadata(table, pandas_metadata)
        810     table, index = _reconstruct_index(table, index_descriptors,
        811                                       all_columns)
    --> 812     ext_columns_dtypes = _get_extension_dtypes(
        813         table, all_columns, types_mapper)
        814 else:
        815     index = _pandas_api.pd.RangeIndex(table.num_rows)
    
    File /opt/miniconda3/envs/pandas-dev/lib/python3.10/site-packages/pyarrow/pandas_compat.py:865, in _get_extension_dtypes(table, columns_metadata, types_mapper)
        860 dtype = col_meta['numpy_type']
        862 if dtype not in _pandas_supported_numpy_types:
        863     # pandas_dtype is expensive, so avoid doing this for types
        864     # that are certainly numpy dtypes
    --> 865     pandas_dtype = _pandas_api.pandas_dtype(dtype)
        866     if isinstance(pandas_dtype, _pandas_api.extension_dtype):
        867         if hasattr(pandas_dtype, "__from_arrow__"):
    
    File /opt/miniconda3/envs/pandas-dev/lib/python3.10/site-packages/pyarrow/pandas-shim.pxi:136, in pyarrow.lib._PandasAPIShim.pandas_dtype()
    
    File /opt/miniconda3/envs/pandas-dev/lib/python3.10/site-packages/pyarrow/pandas-shim.pxi:139, in pyarrow.lib._PandasAPIShim.pandas_dtype()
    
    File ~/.../pandas/core/dtypes/common.py:1626, in pandas_dtype(dtype)
       1621     with warnings.catch_warnings():
       1622         # GH#51523 - Series.astype(np.integer) doesn't show
       1623         # numpy deprecation warning of np.integer
       1624         # Hence enabling DeprecationWarning
       1625         warnings.simplefilter("always", DeprecationWarning)
    -> 1626         npdtype = np.dtype(dtype)
       1627 except SyntaxError as err:
       1628     # np.dtype uses `eval` which can raise SyntaxError
       1629     raise TypeError(f"data type '{dtype}' not understood") from err
    
    TypeError: data type 'list<item: string>[pyarrow]' not understood
  3. added
    Upstream issueIssue related to pandas dependency
    and removed
    Needs TriageIssue that has not been reviewed by a pandas team member
    on May 1, 2023
  4. changed the title [-]BUG: TypeError: data type 'list<item: string>[pyarrow]' not understood[/-] [+]BUG: read_parquet converts pyarrow list type to numpy dtype[/+] on May 1, 2023
  5. danielhanchen commented on May 2, 2023

    @danielhanchen
    Author

    Hmm so I looked at the Pandas code, and not sure if using pd.ArrowDtype(dtype) will work.

    The issue is data.schema.pandas_metadata['columns'][7]["numpy_type"] is a str and not an actual type object, and pd.ArrowDtype does not accept strings.

    eg:

    dt = A.schema.pandas_metadata['columns'][7]["numpy_type"]
    

    returns:

    'list<element: struct<rank: uint8, subtype: dictionary<values=string, indices=int32, ordered=0>, caption: string, credit: string, type: dictionary<values=string, indices=int32, ordered=0>, url: string, height: uint16, width: uint16, subType: dictionary<values=string, indices=int32, ordered=0>, crop_name: dictionary<values=string, indices=int32, ordered=0>>>[pyarrow]'
    

    and using

    pd.ArrowDtype(dt)
    

    fails since it's a string.

    I think the better approach would be to not just pass in data.schema.pandas_metadata['columns'][j]["numpy_type"] but also data.schema.types since it has the actual types which can be converted into a pd.ArrowDtype object.

  6. phofl commented on May 2, 2023

    @phofl
    Member

    I think this behaves as expected. You can pass dtype_backend="pyarrow" to keep the list dtype

  7. danielhanchen commented on May 3, 2023

    @danielhanchen
    Author

    @phofl

    Oh oops I forgot to mention I tried pd.read_parquet(..., dtype_backend = "pyarrow"), and the TypeError still exists. The error is exactly the same, since it passes the dtype to np.dtype

  8. danielhanchen commented on May 3, 2023

    @danielhanchen
    Author

    Confirmed it still fails:

    import pandas as pd
    import pyarrow as pa
    pyarrow_list_of_strings = pd.ArrowDtype(pa.list_(pa.string()))
    data = pd.DataFrame({
        "Pyarrow" : pd.Series([["a"], ["a", "b"]], dtype = pyarrow_list_of_strings),
    })
    data.to_parquet("data.parquet") # SUCCESS
    pd.read_parquet("data.parquet", dtype_backend = "pyarrow") # *** FAIL
    
  9. phofl commented on May 3, 2023

    @phofl
    Member

    Interesting,

    This one works:

    data = pd.DataFrame({
        "Pyarrow" : pd.Series([["a"], ["a", "b"]]),
    })
    data.to_parquet("data.parquet")
    pd.read_parquet("data.parquet", dtype_backend = "pyarrow")
    
  10. danielhanchen commented on May 3, 2023

    @danielhanchen
    Author

    Ye that works since it's an object - Pyarrow indeed saves the data inside the parquet file as list[string].

    The issue is if you explicity parse list[string] directly, it does not work.

    Ie:

    data = pd.DataFrame({
        "Pyarrow" : pd.Series([["a"], ["a", "b"]]),
    })
    data.dtypes
    

    returns

    Pyarrow    object
    dtype: object
    
  11. 16 remaining items

  12. giftculture commented on Feb 8, 2024

    @giftculture

    I'm running into this issue as well:

    Screenshot 2024-02-07 at 19 50 26

  13. giftculture commented on Feb 8, 2024

    @giftculture

    INSTALLED VERSIONS

    commit : f538741
    python : 3.11.7.final.0
    python-bits : 64
    OS : Linux
    OS-release : 4.13.9-300.fc27.x86_64
    Version : #1 SMP Mon Oct 23 13:41:58 UTC 2017
    machine : x86_64
    processor : x86_64
    byteorder : little
    LC_ALL : None
    LANG : en_US.UTF-8
    LOCALE : en_US.UTF-8

    pandas : 2.2.0
    numpy : 1.26.3
    pytz : 2023.4
    dateutil : 2.8.2
    setuptools : 69.0.3
    pip : 23.3.2
    Cython : None
    pytest : None
    hypothesis : None
    sphinx : None
    blosc : None
    feather : None
    xlsxwriter : None
    lxml.etree : None
    html5lib : None
    pymysql : None
    psycopg2 : 2.9.9
    jinja2 : 3.1.3
    IPython : 8.20.0
    pandas_datareader : None
    adbc-driver-postgresql: None
    adbc-driver-sqlite : None
    bs4 : 4.12.2
    bottleneck : None
    dataframe-api-compat : None
    fastparquet : 2023.10.1
    fsspec : 2023.12.2
    gcsfs : None
    matplotlib : 3.8.2
    numba : 0.58.1
    numexpr : None
    odfpy : None
    openpyxl : None
    pandas_gbq : None
    pyarrow : 15.0.0
    pyreadstat : None
    python-calamine : None
    pyxlsb : None
    s3fs : None
    scipy : 1.12.0
    sqlalchemy : 2.0.25
    tables : None
    tabulate : None
    xarray : 2024.1.1
    xlrd : None
    zstandard : None
    tzdata : 2023.4
    qtpy : 2.4.1
    pyqt5 : None

  14. jborman-stonex commented on Apr 4, 2024

    @jborman-stonex

    The only "workaround" at the pandas-level I've found is to set df.to_parquet(..., store_schema=False) for a df containing complex/nested types like a categorical. Are there any plans to get successful dtype roundtripping going forward? Is there anything in the pyarrow library we can leverage here?

    Versions:

    Details
    INSTALLED VERSIONS
    ------------------
    commit                : bdc79c146c2e32f2cab629be240f01658cfb6cc2
    python                : 3.11.6.final.0
    python-bits           : 64
    OS                    : Windows
    OS-release            : 10
    Version               : 10.0.19045
    machine               : AMD64
    processor             : Intel64 Family 6 Model 186 Stepping 2, GenuineIntel
    byteorder             : little
    LC_ALL                : None
    LANG                  : None
    LOCALE                : English_United States.1252
    
    pandas                : 2.2.1
    numpy                 : 1.26.4
    pytz                  : 2024.1
    dateutil              : 2.9.0.post0
    setuptools            : 65.5.0
    pip                   : 23.2.1
    Cython                : None
    pytest                : 8.1.1
    hypothesis            : None
    sphinx                : None
    blosc                 : None
    feather               : None
    xlsxwriter            : None
    lxml.etree            : None
    html5lib              : None
    pymysql               : None
    psycopg2              : 2.9.9
    jinja2                : None
    IPython               : 8.23.0
    pandas_datareader     : None
    adbc-driver-postgresql: None
    adbc-driver-sqlite    : None
    bs4                   : None
    bottleneck            : None
    dataframe-api-compat  : None
    fastparquet           : None
    fsspec                : None
    gcsfs                 : None
    matplotlib            : None
    numba                 : None
    numexpr               : None
    odfpy                 : None
    openpyxl              : None
    pandas_gbq            : None
    pyarrow               : 15.0.2
    pyreadstat            : None
    python-calamine       : None
    pyxlsb                : None
    s3fs                  : None
    scipy                 : None
    sqlalchemy            : None
    tables                : None
    tabulate              : None
    xarray                : None
    xlrd                  : None
    zstandard             : None
    tzdata                : 2024.1
    qtpy                  : None
    pyqt5                 : None
    
  15. phofl commented on Apr 4, 2024

    @phofl
    Member

    I would recommend using dtype_backend="pyarrow"

  16. giftculture commented on Apr 4, 2024

    @giftculture

    I would recommend using dtype_backend="pyarrow"

    @phoff, not sure if you saw from my screenshot, but I did apply the dypte_backend="pyarrow" to the read_parquet method and it still fails, unless I am misunderstanding your suggestion

  17. bretttully commented on Apr 4, 2024

    @bretttully

    The only workaround I have found so far is the following (which works in all cases I have thought of, except round-tripping an empty dataframe with a struct or list type, setting the schema, and not using dtype_backend="pyarrow" when reading back in).

    Would def welcome suggested improvements to this workaround!

    Obv you can write this differently if you don't want a byte string returned, but for us that's what we want.

        def serialize(self, data: pd.DataFrame, **kwargs) -> bytes:
            """see BytesWriter.serialize -- Dump pandas dataframe to parquet bytes"""
            with io.BytesIO() as stream:
    
                schema = kwargs.pop("schema", None)
                all_arrow_types = all(isinstance(t, pd.ArrowDtype) for t in data.dtypes.tolist())
                # An empty dataframe may use default dtypes that are incompatible with the schema.
                # In this case, first cast to object, as the schema can always convert that to the correct type.
                if len(data) == 0 and schema is not None and not all_arrow_types:
                    data = data.astype("object").astype({n: pd.ArrowDtype(schema.field(n).type) for n in schema.names})
                table = pa.Table.from_pandas(data, schema=schema)
    
                # drop pandas from the schema metadata to work around the bug where you can't read struct columns with
                # pandas metadata
                # see https://github.com/pandas-dev/pandas/issues/53011
                metadata = table.schema.metadata
                if b"pandas" in metadata and b"list" in metadata[b"pandas"] or b"struct" in metadata[b"pandas"]:
                    del metadata[b"pandas"]
                    table = table.replace_schema_metadata(metadata)
    
                pq.write_table(table, stream, **kwargs)
                return stream.getvalue()
  18. kinianlo commented on May 21, 2024

    @kinianlo

    This bug is very similar to #57411 where the data type is list[int] instead of list[str].

  19. judahrand commented on Sep 3, 2024

    @judahrand

    Actually a simpler solution is to directly all .replace on the string and replace list<element: to pa.list_(( etc.

    Additionally, list<element: can also be list<item: depending on how the Parquet was written out (see: use_compliant_nested_type argument to pyarrow.parquet.ParquetWriter). And within Arrow itself the containing name be arbitrary (ie. pyarrow.list_(pyarrow.field('some_name', pyarrow.int64())) is a valid type). So the replace would have to be a regex and something like r'list<(.+):' with the capturing group grabbing the name of the field.

  20. bretttully commented on Nov 14, 2024

    @bretttully

    Matching arrow ticket: apache/arrow#39914 and potential PR: apache/arrow#44720

  21. jorisvandenbossche commented on Nov 14, 2024

    @jorisvandenbossche
    Member

    Only seeing this long ticket now .. FWIW I think this is another good reason it would be good pandas had tighter control over the pandas<->arrow conversion (#59780)

  22. added a commit that references this issue on Nov 27, 2024
  23. mikeoconnor0308 commented on Aug 8, 2025

    @mikeoconnor0308

    Is there any update on this ticket? It seems to still be a problem on pyarrow 21.0.0, despite apache/arrow#39914

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Arrowpyarrow functionalityBugIO Parquetparquet, feather

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions