GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py - #45471

Merged
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter
Feb 20, 2025
Merged

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py#45471
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter

Conversation

@jonasdedden

@jonasdeddenjonasdedden commented Feb 9, 2025

Copy link
Copy Markdown
Contributor

Rationale for this change

Currently, unfortunately MapScalar/Array types are not deserialized into proper Python dicts, which is unfortunate since this breaks "roundtrips" from Python -> Arrow -> Python:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
# [{'x': [('a', 1)]}]

This is especially bad when storing TiBs of deeply nested data (think of lists in structs in maps...) that were created from Python and serialized into Arrow/Parquet, since they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds.

What changes are included in this PR?

A new parameter maps_as_pydicts is introduced to to_pylist, to_pydict, as_py which will allow proper roundtrips:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist(maps_as_pydicts="strict")
# [{'x': {'a': 1}}]

Are these changes tested?

Yes. There are tests for to_pylist and to_pydict included for pyarrow.Table, whilst low-level MapScalar and especially a nesting with ListScalar and StructScalar is tested.

Also, duplicate keys now should throw an error, which is also tested for.

Are there any user-facing changes?

Yes. The as_py() method on Scalar instances can be called with a new keyword argument maps_as_pydicts.

As a consequence, if you implement your own Scalar subclass (for example for an extension type), you should change its signature to accept that new argument. For example this definition:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returndeserialize_json(self.value.as_py() ifself.valueelseNone)

could be changed to:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returndeserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

Fix ExampleUuidScalarType
Add tests for `maps_as_pydicts`
Add test for duplicate map keys
Formatting fixes
Add docstring for 'maps_as_pydicts'
Formatting fixes
Call from_arrays from Table
Fix last hopefully issues
Correct MapScalar method "as_py" when there are multiple keys present
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #39010has been automatically assigned in GitHub to PR creator.

@pitrou

Copy link
Copy Markdown
Member

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

@pitrou

Copy link
Copy Markdown
Member

Also:

they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds

Please note that from_pylist and to_pylist are quite costly in themselves. Usually you want to avoid these kinds of roundtrips to/from Python objects if you are concerned with performance.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

Let me clarify what this is about. Map fields are already createable with from_pylist by using list of tuples, as I show in the tests I added. Even the code in my initial message can show this. Fundamentally, it's about adding opt-in behaviour to to_pylist to arrive at a functionality one would expect from a Python perspective:

data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
^---------------------------------------------^
this works fine, data will properly encoded in the Arrow way of encoding Maps
^---------^
this will give lists of tuples instead of dicts 

You can use data = [{'x': [('a', 1)]}] here too, this will yield the same RecordBatch. This then of course technically would qualify as a proper "roundtrip", but this is not what this issue is about, it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

Please note that from_pylist and to_pylist are quite costly in themselves.

Yes, but this is part of a very large distributed machine learning setup, where relatively intricate filters applied on deeply nested list/struct/map columns. The compute of the actual machine learning outclasses the compute one has to do to deserialize Python objects by many orders of magnitude.

For pure data queries, we would not use bare Python objects of course.

@pitrou

Copy link
Copy Markdown
Member

it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

I see, thanks. Then, do we want to reuse the same parameter signature as in the Pandas-related PR? I.e., allow either None, "lossy" and "strict", rather than a boolean.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

allow either None, "lossy" and "strict", rather than a boolean.

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method? This has to be done because the to_pylist method calls as_py on its member arrays (which can be all possible types), and therefore all array/scalar types have to support this parameter. I did not see any other way to easily implement this. I'm willing to do quick progress here, so if you come up with another idea, let me know.

@pitrou

Copy link
Copy Markdown
Member

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method?

That sounds ok to me. Ideally, to_pylist wouldn't call as_py in a loop (which is going to be quite slow), but that would be a major refactor.

@pitrou

Copy link
Copy Markdown
Member

By the way, we probably want to make the new parameter keyword-only?

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I addressed the remarks :) There is some weird error in the "Docs" job, I don't know what this is about.

@pitrou

Copy link
Copy Markdown
Member

Hmm, it looks like some of the CI failures will need #45500 to be merged first

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I rebased the branch, now the CI tests seem fine again, I think?

Could we get a approval/review of this? :)

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jonded94 ! This looks good on the principle, here are some assorted comments.

Comment threadpython/pyarrow/array.pxi Outdated
Comment on lines +1667 to +1668
This can change the ordering of (key, value) pairs, and will
deduplicate multiple keys, resulting in a possible loss of data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the ordering comment is obsolete, as Python dicts are ordered nowadays. Unless the underlying implementation does something weird, ordering should therefore be preserved.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the ordering part, added some explanation of which value survives on duplicate keys.

Comment threadpython/pyarrow/array.pxi
Comment threadpython/pyarrow/table.pxi Outdated
Arrow Map, as in [(key1, value1), (key2, value2), ...].

If 'lossy' or 'strict', convert Arrow Map arrays to native Python dicts.
This can change the ordering of (key, value) pairs, and will

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: ordering

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Comment threadpython/pyarrow/tests/test_scalars.py Outdated
with pytest.raises(ValueError):
assert s.as_py(maps_as_pydicts="strict")

assert s.as_py(maps_as_pydicts="lossy") == {'a': 2}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check that a warning is actually emitted? See pytest.warns

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented a check for this warning

Comment threadpython/pyarrow/scalar.pxi
Comment threadpython/pyarrow/scalar.pxi Outdated
raise ValueError(
"Invalid value for 'maps_as_pydicts': "
+ "valid values are 'lossy', 'strict' or `None` (default). "
+ f"Received '{maps_as_pydicts}'."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it may be more idiomatic to use the repr here

Suggested change
+ f"Received '{maps_as_pydicts}'."
+ f"Received {maps_as_pydicts!r}."

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the suggested change

Comment threadpython/pyarrow/scalar.pxi Outdated
for key, value in self:
if key in result_dict:
if maps_as_pydicts == "strict":
raise ValueError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this a KeyError. Also, the message should perhaps contain the duplicate key?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a KeyError

@pitrou

Copy link
Copy Markdown
Member

@github-actions crossbow submit -g python

@github-actions

Copy link
Copy Markdown

Revision: 93045c4

Submitted crossbow builds: ursacomputing/crossbow @ actions-9728f80818

TaskStatus
example-python-minimal-build-fedora-condaGitHub Actions
example-python-minimal-build-ubuntu-venvGitHub Actions
test-conda-python-3.10GitHub Actions
test-conda-python-3.10-hdfs-2.9.2GitHub Actions
test-conda-python-3.10-hdfs-3.2.1GitHub Actions
test-conda-python-3.10-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11GitHub Actions
test-conda-python-3.11-dask-latestGitHub Actions
test-conda-python-3.11-dask-upstream_develGitHub Actions
test-conda-python-3.11-hypothesisGitHub Actions
test-conda-python-3.11-pandas-latest-numpy-1.26GitHub Actions
test-conda-python-3.11-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11-pandas-nightly-numpy-nightlyGitHub Actions
test-conda-python-3.11-pandas-upstream_devel-numpy-nightlyGitHub Actions
test-conda-python-3.11-spark-masterGitHub Actions
test-conda-python-3.12GitHub Actions
test-conda-python-3.12-cpython-debugGitHub Actions
test-conda-python-3.13GitHub Actions
test-conda-python-3.9GitHub Actions
test-conda-python-3.9-pandas-1.1.3-numpy-1.19.5GitHub Actions
test-conda-python-emscriptenGitHub Actions
test-cuda-python-ubuntu-22.04-cuda-11.7.1GitHub Actions
test-debian-12-python-3-amd64GitHub Actions
test-debian-12-python-3-i386GitHub Actions
test-fedora-39-python-3GitHub Actions
test-ubuntu-22.04-python-3GitHub Actions
test-ubuntu-22.04-python-313-freethreadingGitHub Actions
test-ubuntu-24.04-python-3GitHub Actions

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Feb 20, 2025

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, will merge if CI is green.

@pitrou

Copy link
Copy Markdown
Member

CI failures are unrelated.

@Linchin

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

@omatthew98

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

We (Ray Data team) are also running into backward compatibility issues like this in our tests against pyarrow nightly with the same error mentioned here:

[2025-02-25T06:27:20Z] ===================================FAILURES===================================--| [2025-02-25T06:27:20Z] ____________test_convert_to_pyarrow_array_object_ext_type_fallback____________| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] deftest_convert_to_pyarrow_array_object_ext_type_fallback():
| [2025-02-25T06:27:20Z] column_values=create_ragged_ndarray(
| [2025-02-25T06:27:20Z] [
| [2025-02-25T06:27:20Z] "hi",
| [2025-02-25T06:27:20Z] 1,
| [2025-02-25T06:27:20Z] None,
| [2025-02-25T06:27:20Z] [[[[]]]],
| [2025-02-25T06:27:20Z] {"a": [[{"b": 2, "c": UserObj(i=123)}]]},
| [2025-02-25T06:27:20Z] UserObj(i=456),
| [2025-02-25T06:27:20Z] ]
| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z] column_name="py_object_column"| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # First, assert that straightforward conversion into Arrow native types fails| [2025-02-25T06:27:20Z] withpytest.raises(ArrowConversionError) asexc_info:
| [2025-02-25T06:27:20Z] _convert_to_pyarrow_native_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] assert (
| [2025-02-25T06:27:20Z] str(exc_info.value)
| [2025-02-25T06:27:20Z] =="Error converting data to Arrow: ['hi' 1 None list([[[[]]]]) {'a': [[{'b': 2, 'c': UserObj(i=123)}]]}\n UserObj(i=456)]"# noqa: E501| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # Subsequently, assert that fallback to `ArrowObjectExtensionType` succeeds| [2025-02-25T06:27:20Z] pa_array=convert_to_pyarrow_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] >assertpa_array.to_pylist() ==column_values.tolist()
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] python/ray/air/tests/test_arrow.py:121:
| [2025-02-25T06:27:20Z] _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] > ???
| [2025-02-25T06:27:20Z] ETypeError: as_py() gotanunexpectedkeywordargument'maps_as_pydicts'

@pitrou

Copy link
Copy Markdown
Member

@Linchin@omatthew98 I think the way around this would be to take a **kwargs in your as_py method and then forward it to any nested as_py call (if any).

For example turn this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returnJSONArray._deserialize_json(self.value.as_py() ifself.valueelseNone)

into this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returnJSONArray._deserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

@pitrou

Copy link
Copy Markdown
Member

I've updated the PR description, we should remember to call out this potential incompatibility in the release notes for the next version.

raulchen pushed a commit to ray-project/ray that referenced this pull request Mar 3, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
xsuler pushed a commit to antgroup/ant-ray that referenced this pull request Mar 4, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
abrarsheikh pushed a commit to ray-project/ray that referenced this pull request Mar 8, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Signed-off-by: Abrar Sheikh <abrar@anyscale.com>
park12sj pushed a commit to park12sj/ray that referenced this pull request Mar 18, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jonasdedden@pitrou@Linchin@omatthew98
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py - #45471

Merged
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter
Feb 20, 2025
Merged

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py#45471
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter

Conversation

@jonasdedden

@jonasdeddenjonasdedden commented Feb 9, 2025

Copy link
Copy Markdown
Contributor

Rationale for this change

Currently, unfortunately MapScalar/Array types are not deserialized into proper Python dicts, which is unfortunate since this breaks "roundtrips" from Python -> Arrow -> Python:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
# [{'x': [('a', 1)]}]

This is especially bad when storing TiBs of deeply nested data (think of lists in structs in maps...) that were created from Python and serialized into Arrow/Parquet, since they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds.

What changes are included in this PR?

A new parameter maps_as_pydicts is introduced to to_pylist, to_pydict, as_py which will allow proper roundtrips:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist(maps_as_pydicts="strict")
# [{'x': {'a': 1}}]

Are these changes tested?

Yes. There are tests for to_pylist and to_pydict included for pyarrow.Table, whilst low-level MapScalar and especially a nesting with ListScalar and StructScalar is tested.

Also, duplicate keys now should throw an error, which is also tested for.

Are there any user-facing changes?

Yes. The as_py() method on Scalar instances can be called with a new keyword argument maps_as_pydicts.

As a consequence, if you implement your own Scalar subclass (for example for an extension type), you should change its signature to accept that new argument. For example this definition:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returndeserialize_json(self.value.as_py() ifself.valueelseNone)

could be changed to:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returndeserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

Fix ExampleUuidScalarType
Add tests for `maps_as_pydicts`
Add test for duplicate map keys
Formatting fixes
Add docstring for 'maps_as_pydicts'
Formatting fixes
Call from_arrays from Table
Fix last hopefully issues
Correct MapScalar method "as_py" when there are multiple keys present
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #39010has been automatically assigned in GitHub to PR creator.

@pitrou

Copy link
Copy Markdown
Member

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

@pitrou

Copy link
Copy Markdown
Member

Also:

they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds

Please note that from_pylist and to_pylist are quite costly in themselves. Usually you want to avoid these kinds of roundtrips to/from Python objects if you are concerned with performance.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

Let me clarify what this is about. Map fields are already createable with from_pylist by using list of tuples, as I show in the tests I added. Even the code in my initial message can show this. Fundamentally, it's about adding opt-in behaviour to to_pylist to arrive at a functionality one would expect from a Python perspective:

data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
^---------------------------------------------^
this works fine, data will properly encoded in the Arrow way of encoding Maps
^---------^
this will give lists of tuples instead of dicts 

You can use data = [{'x': [('a', 1)]}] here too, this will yield the same RecordBatch. This then of course technically would qualify as a proper "roundtrip", but this is not what this issue is about, it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

Please note that from_pylist and to_pylist are quite costly in themselves.

Yes, but this is part of a very large distributed machine learning setup, where relatively intricate filters applied on deeply nested list/struct/map columns. The compute of the actual machine learning outclasses the compute one has to do to deserialize Python objects by many orders of magnitude.

For pure data queries, we would not use bare Python objects of course.

@pitrou

Copy link
Copy Markdown
Member

it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

I see, thanks. Then, do we want to reuse the same parameter signature as in the Pandas-related PR? I.e., allow either None, "lossy" and "strict", rather than a boolean.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

allow either None, "lossy" and "strict", rather than a boolean.

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method? This has to be done because the to_pylist method calls as_py on its member arrays (which can be all possible types), and therefore all array/scalar types have to support this parameter. I did not see any other way to easily implement this. I'm willing to do quick progress here, so if you come up with another idea, let me know.

@pitrou

Copy link
Copy Markdown
Member

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method?

That sounds ok to me. Ideally, to_pylist wouldn't call as_py in a loop (which is going to be quite slow), but that would be a major refactor.

@pitrou

Copy link
Copy Markdown
Member

By the way, we probably want to make the new parameter keyword-only?

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I addressed the remarks :) There is some weird error in the "Docs" job, I don't know what this is about.

@pitrou

Copy link
Copy Markdown
Member

Hmm, it looks like some of the CI failures will need #45500 to be merged first

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I rebased the branch, now the CI tests seem fine again, I think?

Could we get a approval/review of this? :)

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jonded94 ! This looks good on the principle, here are some assorted comments.

Comment threadpython/pyarrow/array.pxi Outdated
Comment on lines +1667 to +1668
This can change the ordering of (key, value) pairs, and will
deduplicate multiple keys, resulting in a possible loss of data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the ordering comment is obsolete, as Python dicts are ordered nowadays. Unless the underlying implementation does something weird, ordering should therefore be preserved.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the ordering part, added some explanation of which value survives on duplicate keys.

Comment threadpython/pyarrow/array.pxi
Comment threadpython/pyarrow/table.pxi Outdated
Arrow Map, as in [(key1, value1), (key2, value2), ...].

If 'lossy' or 'strict', convert Arrow Map arrays to native Python dicts.
This can change the ordering of (key, value) pairs, and will

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: ordering

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Comment threadpython/pyarrow/tests/test_scalars.py Outdated
with pytest.raises(ValueError):
assert s.as_py(maps_as_pydicts="strict")

assert s.as_py(maps_as_pydicts="lossy") == {'a': 2}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check that a warning is actually emitted? See pytest.warns

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented a check for this warning

Comment threadpython/pyarrow/scalar.pxi
Comment threadpython/pyarrow/scalar.pxi Outdated
raise ValueError(
"Invalid value for 'maps_as_pydicts': "
+ "valid values are 'lossy', 'strict' or `None` (default). "
+ f"Received '{maps_as_pydicts}'."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it may be more idiomatic to use the repr here

Suggested change
+ f"Received '{maps_as_pydicts}'."
+ f"Received {maps_as_pydicts!r}."

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the suggested change

Comment threadpython/pyarrow/scalar.pxi Outdated
for key, value in self:
if key in result_dict:
if maps_as_pydicts == "strict":
raise ValueError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this a KeyError. Also, the message should perhaps contain the duplicate key?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a KeyError

@pitrou

Copy link
Copy Markdown
Member

@github-actions crossbow submit -g python

@github-actions

Copy link
Copy Markdown

Revision: 93045c4

Submitted crossbow builds: ursacomputing/crossbow @ actions-9728f80818

TaskStatus
example-python-minimal-build-fedora-condaGitHub Actions
example-python-minimal-build-ubuntu-venvGitHub Actions
test-conda-python-3.10GitHub Actions
test-conda-python-3.10-hdfs-2.9.2GitHub Actions
test-conda-python-3.10-hdfs-3.2.1GitHub Actions
test-conda-python-3.10-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11GitHub Actions
test-conda-python-3.11-dask-latestGitHub Actions
test-conda-python-3.11-dask-upstream_develGitHub Actions
test-conda-python-3.11-hypothesisGitHub Actions
test-conda-python-3.11-pandas-latest-numpy-1.26GitHub Actions
test-conda-python-3.11-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11-pandas-nightly-numpy-nightlyGitHub Actions
test-conda-python-3.11-pandas-upstream_devel-numpy-nightlyGitHub Actions
test-conda-python-3.11-spark-masterGitHub Actions
test-conda-python-3.12GitHub Actions
test-conda-python-3.12-cpython-debugGitHub Actions
test-conda-python-3.13GitHub Actions
test-conda-python-3.9GitHub Actions
test-conda-python-3.9-pandas-1.1.3-numpy-1.19.5GitHub Actions
test-conda-python-emscriptenGitHub Actions
test-cuda-python-ubuntu-22.04-cuda-11.7.1GitHub Actions
test-debian-12-python-3-amd64GitHub Actions
test-debian-12-python-3-i386GitHub Actions
test-fedora-39-python-3GitHub Actions
test-ubuntu-22.04-python-3GitHub Actions
test-ubuntu-22.04-python-313-freethreadingGitHub Actions
test-ubuntu-24.04-python-3GitHub Actions

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Feb 20, 2025

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, will merge if CI is green.

@pitrou

Copy link
Copy Markdown
Member

CI failures are unrelated.

@Linchin

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

@omatthew98

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

We (Ray Data team) are also running into backward compatibility issues like this in our tests against pyarrow nightly with the same error mentioned here:

[2025-02-25T06:27:20Z] ===================================FAILURES===================================--| [2025-02-25T06:27:20Z] ____________test_convert_to_pyarrow_array_object_ext_type_fallback____________| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] deftest_convert_to_pyarrow_array_object_ext_type_fallback():
| [2025-02-25T06:27:20Z] column_values=create_ragged_ndarray(
| [2025-02-25T06:27:20Z] [
| [2025-02-25T06:27:20Z] "hi",
| [2025-02-25T06:27:20Z] 1,
| [2025-02-25T06:27:20Z] None,
| [2025-02-25T06:27:20Z] [[[[]]]],
| [2025-02-25T06:27:20Z] {"a": [[{"b": 2, "c": UserObj(i=123)}]]},
| [2025-02-25T06:27:20Z] UserObj(i=456),
| [2025-02-25T06:27:20Z] ]
| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z] column_name="py_object_column"| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # First, assert that straightforward conversion into Arrow native types fails| [2025-02-25T06:27:20Z] withpytest.raises(ArrowConversionError) asexc_info:
| [2025-02-25T06:27:20Z] _convert_to_pyarrow_native_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] assert (
| [2025-02-25T06:27:20Z] str(exc_info.value)
| [2025-02-25T06:27:20Z] =="Error converting data to Arrow: ['hi' 1 None list([[[[]]]]) {'a': [[{'b': 2, 'c': UserObj(i=123)}]]}\n UserObj(i=456)]"# noqa: E501| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # Subsequently, assert that fallback to `ArrowObjectExtensionType` succeeds| [2025-02-25T06:27:20Z] pa_array=convert_to_pyarrow_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] >assertpa_array.to_pylist() ==column_values.tolist()
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] python/ray/air/tests/test_arrow.py:121:
| [2025-02-25T06:27:20Z] _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] > ???
| [2025-02-25T06:27:20Z] ETypeError: as_py() gotanunexpectedkeywordargument'maps_as_pydicts'

@pitrou

Copy link
Copy Markdown
Member

@Linchin@omatthew98 I think the way around this would be to take a **kwargs in your as_py method and then forward it to any nested as_py call (if any).

For example turn this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returnJSONArray._deserialize_json(self.value.as_py() ifself.valueelseNone)

into this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returnJSONArray._deserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

@pitrou

Copy link
Copy Markdown
Member

I've updated the PR description, we should remember to call out this potential incompatibility in the release notes for the next version.

raulchen pushed a commit to ray-project/ray that referenced this pull request Mar 3, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
xsuler pushed a commit to antgroup/ant-ray that referenced this pull request Mar 4, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
abrarsheikh pushed a commit to ray-project/ray that referenced this pull request Mar 8, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Signed-off-by: Abrar Sheikh <abrar@anyscale.com>
park12sj pushed a commit to park12sj/ray that referenced this pull request Mar 18, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jonasdedden@pitrou@Linchin@omatthew98
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py - #45471

Merged
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter
Feb 20, 2025
Merged

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py#45471
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter

Conversation

@jonasdedden

@jonasdeddenjonasdedden commented Feb 9, 2025

Copy link
Copy Markdown
Contributor

Rationale for this change

Currently, unfortunately MapScalar/Array types are not deserialized into proper Python dicts, which is unfortunate since this breaks "roundtrips" from Python -> Arrow -> Python:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
# [{'x': [('a', 1)]}]

This is especially bad when storing TiBs of deeply nested data (think of lists in structs in maps...) that were created from Python and serialized into Arrow/Parquet, since they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds.

What changes are included in this PR?

A new parameter maps_as_pydicts is introduced to to_pylist, to_pydict, as_py which will allow proper roundtrips:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist(maps_as_pydicts="strict")
# [{'x': {'a': 1}}]

Are these changes tested?

Yes. There are tests for to_pylist and to_pydict included for pyarrow.Table, whilst low-level MapScalar and especially a nesting with ListScalar and StructScalar is tested.

Also, duplicate keys now should throw an error, which is also tested for.

Are there any user-facing changes?

Yes. The as_py() method on Scalar instances can be called with a new keyword argument maps_as_pydicts.

As a consequence, if you implement your own Scalar subclass (for example for an extension type), you should change its signature to accept that new argument. For example this definition:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returndeserialize_json(self.value.as_py() ifself.valueelseNone)

could be changed to:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returndeserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

Fix ExampleUuidScalarType
Add tests for `maps_as_pydicts`
Add test for duplicate map keys
Formatting fixes
Add docstring for 'maps_as_pydicts'
Formatting fixes
Call from_arrays from Table
Fix last hopefully issues
Correct MapScalar method "as_py" when there are multiple keys present
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #39010has been automatically assigned in GitHub to PR creator.

@pitrou

Copy link
Copy Markdown
Member

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

@pitrou

Copy link
Copy Markdown
Member

Also:

they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds

Please note that from_pylist and to_pylist are quite costly in themselves. Usually you want to avoid these kinds of roundtrips to/from Python objects if you are concerned with performance.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

Let me clarify what this is about. Map fields are already createable with from_pylist by using list of tuples, as I show in the tests I added. Even the code in my initial message can show this. Fundamentally, it's about adding opt-in behaviour to to_pylist to arrive at a functionality one would expect from a Python perspective:

data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
^---------------------------------------------^
this works fine, data will properly encoded in the Arrow way of encoding Maps
^---------^
this will give lists of tuples instead of dicts 

You can use data = [{'x': [('a', 1)]}] here too, this will yield the same RecordBatch. This then of course technically would qualify as a proper "roundtrip", but this is not what this issue is about, it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

Please note that from_pylist and to_pylist are quite costly in themselves.

Yes, but this is part of a very large distributed machine learning setup, where relatively intricate filters applied on deeply nested list/struct/map columns. The compute of the actual machine learning outclasses the compute one has to do to deserialize Python objects by many orders of magnitude.

For pure data queries, we would not use bare Python objects of course.

@pitrou

Copy link
Copy Markdown
Member

it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

I see, thanks. Then, do we want to reuse the same parameter signature as in the Pandas-related PR? I.e., allow either None, "lossy" and "strict", rather than a boolean.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

allow either None, "lossy" and "strict", rather than a boolean.

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method? This has to be done because the to_pylist method calls as_py on its member arrays (which can be all possible types), and therefore all array/scalar types have to support this parameter. I did not see any other way to easily implement this. I'm willing to do quick progress here, so if you come up with another idea, let me know.

@pitrou

Copy link
Copy Markdown
Member

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method?

That sounds ok to me. Ideally, to_pylist wouldn't call as_py in a loop (which is going to be quite slow), but that would be a major refactor.

@pitrou

Copy link
Copy Markdown
Member

By the way, we probably want to make the new parameter keyword-only?

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I addressed the remarks :) There is some weird error in the "Docs" job, I don't know what this is about.

@pitrou

Copy link
Copy Markdown
Member

Hmm, it looks like some of the CI failures will need #45500 to be merged first

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I rebased the branch, now the CI tests seem fine again, I think?

Could we get a approval/review of this? :)

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jonded94 ! This looks good on the principle, here are some assorted comments.

Comment threadpython/pyarrow/array.pxi Outdated
Comment on lines +1667 to +1668
This can change the ordering of (key, value) pairs, and will
deduplicate multiple keys, resulting in a possible loss of data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the ordering comment is obsolete, as Python dicts are ordered nowadays. Unless the underlying implementation does something weird, ordering should therefore be preserved.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the ordering part, added some explanation of which value survives on duplicate keys.

Comment threadpython/pyarrow/array.pxi
Comment threadpython/pyarrow/table.pxi Outdated
Arrow Map, as in [(key1, value1), (key2, value2), ...].

If 'lossy' or 'strict', convert Arrow Map arrays to native Python dicts.
This can change the ordering of (key, value) pairs, and will

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: ordering

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Comment threadpython/pyarrow/tests/test_scalars.py Outdated
with pytest.raises(ValueError):
assert s.as_py(maps_as_pydicts="strict")

assert s.as_py(maps_as_pydicts="lossy") == {'a': 2}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check that a warning is actually emitted? See pytest.warns

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented a check for this warning

Comment threadpython/pyarrow/scalar.pxi
Comment threadpython/pyarrow/scalar.pxi Outdated
raise ValueError(
"Invalid value for 'maps_as_pydicts': "
+ "valid values are 'lossy', 'strict' or `None` (default). "
+ f"Received '{maps_as_pydicts}'."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it may be more idiomatic to use the repr here

Suggested change
+ f"Received '{maps_as_pydicts}'."
+ f"Received {maps_as_pydicts!r}."

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the suggested change

Comment threadpython/pyarrow/scalar.pxi Outdated
for key, value in self:
if key in result_dict:
if maps_as_pydicts == "strict":
raise ValueError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this a KeyError. Also, the message should perhaps contain the duplicate key?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a KeyError

@pitrou

Copy link
Copy Markdown
Member

@github-actions crossbow submit -g python

@github-actions

Copy link
Copy Markdown

Revision: 93045c4

Submitted crossbow builds: ursacomputing/crossbow @ actions-9728f80818

TaskStatus
example-python-minimal-build-fedora-condaGitHub Actions
example-python-minimal-build-ubuntu-venvGitHub Actions
test-conda-python-3.10GitHub Actions
test-conda-python-3.10-hdfs-2.9.2GitHub Actions
test-conda-python-3.10-hdfs-3.2.1GitHub Actions
test-conda-python-3.10-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11GitHub Actions
test-conda-python-3.11-dask-latestGitHub Actions
test-conda-python-3.11-dask-upstream_develGitHub Actions
test-conda-python-3.11-hypothesisGitHub Actions
test-conda-python-3.11-pandas-latest-numpy-1.26GitHub Actions
test-conda-python-3.11-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11-pandas-nightly-numpy-nightlyGitHub Actions
test-conda-python-3.11-pandas-upstream_devel-numpy-nightlyGitHub Actions
test-conda-python-3.11-spark-masterGitHub Actions
test-conda-python-3.12GitHub Actions
test-conda-python-3.12-cpython-debugGitHub Actions
test-conda-python-3.13GitHub Actions
test-conda-python-3.9GitHub Actions
test-conda-python-3.9-pandas-1.1.3-numpy-1.19.5GitHub Actions
test-conda-python-emscriptenGitHub Actions
test-cuda-python-ubuntu-22.04-cuda-11.7.1GitHub Actions
test-debian-12-python-3-amd64GitHub Actions
test-debian-12-python-3-i386GitHub Actions
test-fedora-39-python-3GitHub Actions
test-ubuntu-22.04-python-3GitHub Actions
test-ubuntu-22.04-python-313-freethreadingGitHub Actions
test-ubuntu-24.04-python-3GitHub Actions

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Feb 20, 2025

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, will merge if CI is green.

@pitrou

Copy link
Copy Markdown
Member

CI failures are unrelated.

@Linchin

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

@omatthew98

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

We (Ray Data team) are also running into backward compatibility issues like this in our tests against pyarrow nightly with the same error mentioned here:

[2025-02-25T06:27:20Z] ===================================FAILURES===================================--| [2025-02-25T06:27:20Z] ____________test_convert_to_pyarrow_array_object_ext_type_fallback____________| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] deftest_convert_to_pyarrow_array_object_ext_type_fallback():
| [2025-02-25T06:27:20Z] column_values=create_ragged_ndarray(
| [2025-02-25T06:27:20Z] [
| [2025-02-25T06:27:20Z] "hi",
| [2025-02-25T06:27:20Z] 1,
| [2025-02-25T06:27:20Z] None,
| [2025-02-25T06:27:20Z] [[[[]]]],
| [2025-02-25T06:27:20Z] {"a": [[{"b": 2, "c": UserObj(i=123)}]]},
| [2025-02-25T06:27:20Z] UserObj(i=456),
| [2025-02-25T06:27:20Z] ]
| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z] column_name="py_object_column"| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # First, assert that straightforward conversion into Arrow native types fails| [2025-02-25T06:27:20Z] withpytest.raises(ArrowConversionError) asexc_info:
| [2025-02-25T06:27:20Z] _convert_to_pyarrow_native_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] assert (
| [2025-02-25T06:27:20Z] str(exc_info.value)
| [2025-02-25T06:27:20Z] =="Error converting data to Arrow: ['hi' 1 None list([[[[]]]]) {'a': [[{'b': 2, 'c': UserObj(i=123)}]]}\n UserObj(i=456)]"# noqa: E501| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # Subsequently, assert that fallback to `ArrowObjectExtensionType` succeeds| [2025-02-25T06:27:20Z] pa_array=convert_to_pyarrow_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] >assertpa_array.to_pylist() ==column_values.tolist()
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] python/ray/air/tests/test_arrow.py:121:
| [2025-02-25T06:27:20Z] _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] > ???
| [2025-02-25T06:27:20Z] ETypeError: as_py() gotanunexpectedkeywordargument'maps_as_pydicts'

@pitrou

Copy link
Copy Markdown
Member

@Linchin@omatthew98 I think the way around this would be to take a **kwargs in your as_py method and then forward it to any nested as_py call (if any).

For example turn this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returnJSONArray._deserialize_json(self.value.as_py() ifself.valueelseNone)

into this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returnJSONArray._deserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

@pitrou

Copy link
Copy Markdown
Member

I've updated the PR description, we should remember to call out this potential incompatibility in the release notes for the next version.

raulchen pushed a commit to ray-project/ray that referenced this pull request Mar 3, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
xsuler pushed a commit to antgroup/ant-ray that referenced this pull request Mar 4, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
abrarsheikh pushed a commit to ray-project/ray that referenced this pull request Mar 8, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Signed-off-by: Abrar Sheikh <abrar@anyscale.com>
park12sj pushed a commit to park12sj/ray that referenced this pull request Mar 18, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jonasdedden@pitrou@Linchin@omatthew98
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py - #45471

Merged
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter
Feb 20, 2025
Merged

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py#45471
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter

Conversation

@jonasdedden

@jonasdeddenjonasdedden commented Feb 9, 2025

Copy link
Copy Markdown
Contributor

Rationale for this change

Currently, unfortunately MapScalar/Array types are not deserialized into proper Python dicts, which is unfortunate since this breaks "roundtrips" from Python -> Arrow -> Python:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
# [{'x': [('a', 1)]}]

This is especially bad when storing TiBs of deeply nested data (think of lists in structs in maps...) that were created from Python and serialized into Arrow/Parquet, since they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds.

What changes are included in this PR?

A new parameter maps_as_pydicts is introduced to to_pylist, to_pydict, as_py which will allow proper roundtrips:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist(maps_as_pydicts="strict")
# [{'x': {'a': 1}}]

Are these changes tested?

Yes. There are tests for to_pylist and to_pydict included for pyarrow.Table, whilst low-level MapScalar and especially a nesting with ListScalar and StructScalar is tested.

Also, duplicate keys now should throw an error, which is also tested for.

Are there any user-facing changes?

Yes. The as_py() method on Scalar instances can be called with a new keyword argument maps_as_pydicts.

As a consequence, if you implement your own Scalar subclass (for example for an extension type), you should change its signature to accept that new argument. For example this definition:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returndeserialize_json(self.value.as_py() ifself.valueelseNone)

could be changed to:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returndeserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

Fix ExampleUuidScalarType
Add tests for `maps_as_pydicts`
Add test for duplicate map keys
Formatting fixes
Add docstring for 'maps_as_pydicts'
Formatting fixes
Call from_arrays from Table
Fix last hopefully issues
Correct MapScalar method "as_py" when there are multiple keys present
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #39010has been automatically assigned in GitHub to PR creator.

@pitrou

Copy link
Copy Markdown
Member

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

@pitrou

Copy link
Copy Markdown
Member

Also:

they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds

Please note that from_pylist and to_pylist are quite costly in themselves. Usually you want to avoid these kinds of roundtrips to/from Python objects if you are concerned with performance.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

Let me clarify what this is about. Map fields are already createable with from_pylist by using list of tuples, as I show in the tests I added. Even the code in my initial message can show this. Fundamentally, it's about adding opt-in behaviour to to_pylist to arrive at a functionality one would expect from a Python perspective:

data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
^---------------------------------------------^
this works fine, data will properly encoded in the Arrow way of encoding Maps
^---------^
this will give lists of tuples instead of dicts 

You can use data = [{'x': [('a', 1)]}] here too, this will yield the same RecordBatch. This then of course technically would qualify as a proper "roundtrip", but this is not what this issue is about, it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

Please note that from_pylist and to_pylist are quite costly in themselves.

Yes, but this is part of a very large distributed machine learning setup, where relatively intricate filters applied on deeply nested list/struct/map columns. The compute of the actual machine learning outclasses the compute one has to do to deserialize Python objects by many orders of magnitude.

For pure data queries, we would not use bare Python objects of course.

@pitrou

Copy link
Copy Markdown
Member

it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

I see, thanks. Then, do we want to reuse the same parameter signature as in the Pandas-related PR? I.e., allow either None, "lossy" and "strict", rather than a boolean.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

allow either None, "lossy" and "strict", rather than a boolean.

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method? This has to be done because the to_pylist method calls as_py on its member arrays (which can be all possible types), and therefore all array/scalar types have to support this parameter. I did not see any other way to easily implement this. I'm willing to do quick progress here, so if you come up with another idea, let me know.

@pitrou

Copy link
Copy Markdown
Member

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method?

That sounds ok to me. Ideally, to_pylist wouldn't call as_py in a loop (which is going to be quite slow), but that would be a major refactor.

@pitrou

Copy link
Copy Markdown
Member

By the way, we probably want to make the new parameter keyword-only?

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I addressed the remarks :) There is some weird error in the "Docs" job, I don't know what this is about.

@pitrou

Copy link
Copy Markdown
Member

Hmm, it looks like some of the CI failures will need #45500 to be merged first

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I rebased the branch, now the CI tests seem fine again, I think?

Could we get a approval/review of this? :)

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jonded94 ! This looks good on the principle, here are some assorted comments.

Comment threadpython/pyarrow/array.pxi Outdated
Comment on lines +1667 to +1668
This can change the ordering of (key, value) pairs, and will
deduplicate multiple keys, resulting in a possible loss of data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the ordering comment is obsolete, as Python dicts are ordered nowadays. Unless the underlying implementation does something weird, ordering should therefore be preserved.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the ordering part, added some explanation of which value survives on duplicate keys.

Comment threadpython/pyarrow/array.pxi
Comment threadpython/pyarrow/table.pxi Outdated
Arrow Map, as in [(key1, value1), (key2, value2), ...].

If 'lossy' or 'strict', convert Arrow Map arrays to native Python dicts.
This can change the ordering of (key, value) pairs, and will

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: ordering

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Comment threadpython/pyarrow/tests/test_scalars.py Outdated
with pytest.raises(ValueError):
assert s.as_py(maps_as_pydicts="strict")

assert s.as_py(maps_as_pydicts="lossy") == {'a': 2}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check that a warning is actually emitted? See pytest.warns

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented a check for this warning

Comment threadpython/pyarrow/scalar.pxi
Comment threadpython/pyarrow/scalar.pxi Outdated
raise ValueError(
"Invalid value for 'maps_as_pydicts': "
+ "valid values are 'lossy', 'strict' or `None` (default). "
+ f"Received '{maps_as_pydicts}'."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it may be more idiomatic to use the repr here

Suggested change
+ f"Received '{maps_as_pydicts}'."
+ f"Received {maps_as_pydicts!r}."

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the suggested change

Comment threadpython/pyarrow/scalar.pxi Outdated
for key, value in self:
if key in result_dict:
if maps_as_pydicts == "strict":
raise ValueError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this a KeyError. Also, the message should perhaps contain the duplicate key?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a KeyError

@pitrou

Copy link
Copy Markdown
Member

@github-actions crossbow submit -g python

@github-actions

Copy link
Copy Markdown

Revision: 93045c4

Submitted crossbow builds: ursacomputing/crossbow @ actions-9728f80818

TaskStatus
example-python-minimal-build-fedora-condaGitHub Actions
example-python-minimal-build-ubuntu-venvGitHub Actions
test-conda-python-3.10GitHub Actions
test-conda-python-3.10-hdfs-2.9.2GitHub Actions
test-conda-python-3.10-hdfs-3.2.1GitHub Actions
test-conda-python-3.10-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11GitHub Actions
test-conda-python-3.11-dask-latestGitHub Actions
test-conda-python-3.11-dask-upstream_develGitHub Actions
test-conda-python-3.11-hypothesisGitHub Actions
test-conda-python-3.11-pandas-latest-numpy-1.26GitHub Actions
test-conda-python-3.11-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11-pandas-nightly-numpy-nightlyGitHub Actions
test-conda-python-3.11-pandas-upstream_devel-numpy-nightlyGitHub Actions
test-conda-python-3.11-spark-masterGitHub Actions
test-conda-python-3.12GitHub Actions
test-conda-python-3.12-cpython-debugGitHub Actions
test-conda-python-3.13GitHub Actions
test-conda-python-3.9GitHub Actions
test-conda-python-3.9-pandas-1.1.3-numpy-1.19.5GitHub Actions
test-conda-python-emscriptenGitHub Actions
test-cuda-python-ubuntu-22.04-cuda-11.7.1GitHub Actions
test-debian-12-python-3-amd64GitHub Actions
test-debian-12-python-3-i386GitHub Actions
test-fedora-39-python-3GitHub Actions
test-ubuntu-22.04-python-3GitHub Actions
test-ubuntu-22.04-python-313-freethreadingGitHub Actions
test-ubuntu-24.04-python-3GitHub Actions

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Feb 20, 2025

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, will merge if CI is green.

@pitrou

Copy link
Copy Markdown
Member

CI failures are unrelated.

@Linchin

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

@omatthew98

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

We (Ray Data team) are also running into backward compatibility issues like this in our tests against pyarrow nightly with the same error mentioned here:

[2025-02-25T06:27:20Z] ===================================FAILURES===================================--| [2025-02-25T06:27:20Z] ____________test_convert_to_pyarrow_array_object_ext_type_fallback____________| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] deftest_convert_to_pyarrow_array_object_ext_type_fallback():
| [2025-02-25T06:27:20Z] column_values=create_ragged_ndarray(
| [2025-02-25T06:27:20Z] [
| [2025-02-25T06:27:20Z] "hi",
| [2025-02-25T06:27:20Z] 1,
| [2025-02-25T06:27:20Z] None,
| [2025-02-25T06:27:20Z] [[[[]]]],
| [2025-02-25T06:27:20Z] {"a": [[{"b": 2, "c": UserObj(i=123)}]]},
| [2025-02-25T06:27:20Z] UserObj(i=456),
| [2025-02-25T06:27:20Z] ]
| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z] column_name="py_object_column"| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # First, assert that straightforward conversion into Arrow native types fails| [2025-02-25T06:27:20Z] withpytest.raises(ArrowConversionError) asexc_info:
| [2025-02-25T06:27:20Z] _convert_to_pyarrow_native_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] assert (
| [2025-02-25T06:27:20Z] str(exc_info.value)
| [2025-02-25T06:27:20Z] =="Error converting data to Arrow: ['hi' 1 None list([[[[]]]]) {'a': [[{'b': 2, 'c': UserObj(i=123)}]]}\n UserObj(i=456)]"# noqa: E501| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # Subsequently, assert that fallback to `ArrowObjectExtensionType` succeeds| [2025-02-25T06:27:20Z] pa_array=convert_to_pyarrow_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] >assertpa_array.to_pylist() ==column_values.tolist()
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] python/ray/air/tests/test_arrow.py:121:
| [2025-02-25T06:27:20Z] _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] > ???
| [2025-02-25T06:27:20Z] ETypeError: as_py() gotanunexpectedkeywordargument'maps_as_pydicts'

@pitrou

Copy link
Copy Markdown
Member

@Linchin@omatthew98 I think the way around this would be to take a **kwargs in your as_py method and then forward it to any nested as_py call (if any).

For example turn this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returnJSONArray._deserialize_json(self.value.as_py() ifself.valueelseNone)

into this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returnJSONArray._deserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

@pitrou

Copy link
Copy Markdown
Member

I've updated the PR description, we should remember to call out this potential incompatibility in the release notes for the next version.

raulchen pushed a commit to ray-project/ray that referenced this pull request Mar 3, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
xsuler pushed a commit to antgroup/ant-ray that referenced this pull request Mar 4, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
abrarsheikh pushed a commit to ray-project/ray that referenced this pull request Mar 8, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Signed-off-by: Abrar Sheikh <abrar@anyscale.com>
park12sj pushed a commit to park12sj/ray that referenced this pull request Mar 18, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jonasdedden@pitrou@Linchin@omatthew98
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py - #45471

Merged
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter
Feb 20, 2025
Merged

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py#45471
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter

Conversation

@jonasdedden

@jonasdeddenjonasdedden commented Feb 9, 2025

Copy link
Copy Markdown
Contributor

Rationale for this change

Currently, unfortunately MapScalar/Array types are not deserialized into proper Python dicts, which is unfortunate since this breaks "roundtrips" from Python -> Arrow -> Python:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
# [{'x': [('a', 1)]}]

This is especially bad when storing TiBs of deeply nested data (think of lists in structs in maps...) that were created from Python and serialized into Arrow/Parquet, since they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds.

What changes are included in this PR?

A new parameter maps_as_pydicts is introduced to to_pylist, to_pydict, as_py which will allow proper roundtrips:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist(maps_as_pydicts="strict")
# [{'x': {'a': 1}}]

Are these changes tested?

Yes. There are tests for to_pylist and to_pydict included for pyarrow.Table, whilst low-level MapScalar and especially a nesting with ListScalar and StructScalar is tested.

Also, duplicate keys now should throw an error, which is also tested for.

Are there any user-facing changes?

Yes. The as_py() method on Scalar instances can be called with a new keyword argument maps_as_pydicts.

As a consequence, if you implement your own Scalar subclass (for example for an extension type), you should change its signature to accept that new argument. For example this definition:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returndeserialize_json(self.value.as_py() ifself.valueelseNone)

could be changed to:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returndeserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

Fix ExampleUuidScalarType
Add tests for `maps_as_pydicts`
Add test for duplicate map keys
Formatting fixes
Add docstring for 'maps_as_pydicts'
Formatting fixes
Call from_arrays from Table
Fix last hopefully issues
Correct MapScalar method "as_py" when there are multiple keys present
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #39010has been automatically assigned in GitHub to PR creator.

@pitrou

Copy link
Copy Markdown
Member

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

@pitrou

Copy link
Copy Markdown
Member

Also:

they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds

Please note that from_pylist and to_pylist are quite costly in themselves. Usually you want to avoid these kinds of roundtrips to/from Python objects if you are concerned with performance.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

Let me clarify what this is about. Map fields are already createable with from_pylist by using list of tuples, as I show in the tests I added. Even the code in my initial message can show this. Fundamentally, it's about adding opt-in behaviour to to_pylist to arrive at a functionality one would expect from a Python perspective:

data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
^---------------------------------------------^
this works fine, data will properly encoded in the Arrow way of encoding Maps
^---------^
this will give lists of tuples instead of dicts 

You can use data = [{'x': [('a', 1)]}] here too, this will yield the same RecordBatch. This then of course technically would qualify as a proper "roundtrip", but this is not what this issue is about, it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

Please note that from_pylist and to_pylist are quite costly in themselves.

Yes, but this is part of a very large distributed machine learning setup, where relatively intricate filters applied on deeply nested list/struct/map columns. The compute of the actual machine learning outclasses the compute one has to do to deserialize Python objects by many orders of magnitude.

For pure data queries, we would not use bare Python objects of course.

@pitrou

Copy link
Copy Markdown
Member

it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

I see, thanks. Then, do we want to reuse the same parameter signature as in the Pandas-related PR? I.e., allow either None, "lossy" and "strict", rather than a boolean.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

allow either None, "lossy" and "strict", rather than a boolean.

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method? This has to be done because the to_pylist method calls as_py on its member arrays (which can be all possible types), and therefore all array/scalar types have to support this parameter. I did not see any other way to easily implement this. I'm willing to do quick progress here, so if you come up with another idea, let me know.

@pitrou

Copy link
Copy Markdown
Member

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method?

That sounds ok to me. Ideally, to_pylist wouldn't call as_py in a loop (which is going to be quite slow), but that would be a major refactor.

@pitrou

Copy link
Copy Markdown
Member

By the way, we probably want to make the new parameter keyword-only?

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I addressed the remarks :) There is some weird error in the "Docs" job, I don't know what this is about.

@pitrou

Copy link
Copy Markdown
Member

Hmm, it looks like some of the CI failures will need #45500 to be merged first

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I rebased the branch, now the CI tests seem fine again, I think?

Could we get a approval/review of this? :)

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jonded94 ! This looks good on the principle, here are some assorted comments.

Comment threadpython/pyarrow/array.pxi Outdated
Comment on lines +1667 to +1668
This can change the ordering of (key, value) pairs, and will
deduplicate multiple keys, resulting in a possible loss of data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the ordering comment is obsolete, as Python dicts are ordered nowadays. Unless the underlying implementation does something weird, ordering should therefore be preserved.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the ordering part, added some explanation of which value survives on duplicate keys.

Comment threadpython/pyarrow/array.pxi
Comment threadpython/pyarrow/table.pxi Outdated
Arrow Map, as in [(key1, value1), (key2, value2), ...].

If 'lossy' or 'strict', convert Arrow Map arrays to native Python dicts.
This can change the ordering of (key, value) pairs, and will

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: ordering

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Comment threadpython/pyarrow/tests/test_scalars.py Outdated
with pytest.raises(ValueError):
assert s.as_py(maps_as_pydicts="strict")

assert s.as_py(maps_as_pydicts="lossy") == {'a': 2}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check that a warning is actually emitted? See pytest.warns

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented a check for this warning

Comment threadpython/pyarrow/scalar.pxi
Comment threadpython/pyarrow/scalar.pxi Outdated
raise ValueError(
"Invalid value for 'maps_as_pydicts': "
+ "valid values are 'lossy', 'strict' or `None` (default). "
+ f"Received '{maps_as_pydicts}'."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it may be more idiomatic to use the repr here

Suggested change
+ f"Received '{maps_as_pydicts}'."
+ f"Received {maps_as_pydicts!r}."

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the suggested change

Comment threadpython/pyarrow/scalar.pxi Outdated
for key, value in self:
if key in result_dict:
if maps_as_pydicts == "strict":
raise ValueError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this a KeyError. Also, the message should perhaps contain the duplicate key?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a KeyError

@pitrou

Copy link
Copy Markdown
Member

@github-actions crossbow submit -g python

@github-actions

Copy link
Copy Markdown

Revision: 93045c4

Submitted crossbow builds: ursacomputing/crossbow @ actions-9728f80818

TaskStatus
example-python-minimal-build-fedora-condaGitHub Actions
example-python-minimal-build-ubuntu-venvGitHub Actions
test-conda-python-3.10GitHub Actions
test-conda-python-3.10-hdfs-2.9.2GitHub Actions
test-conda-python-3.10-hdfs-3.2.1GitHub Actions
test-conda-python-3.10-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11GitHub Actions
test-conda-python-3.11-dask-latestGitHub Actions
test-conda-python-3.11-dask-upstream_develGitHub Actions
test-conda-python-3.11-hypothesisGitHub Actions
test-conda-python-3.11-pandas-latest-numpy-1.26GitHub Actions
test-conda-python-3.11-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11-pandas-nightly-numpy-nightlyGitHub Actions
test-conda-python-3.11-pandas-upstream_devel-numpy-nightlyGitHub Actions
test-conda-python-3.11-spark-masterGitHub Actions
test-conda-python-3.12GitHub Actions
test-conda-python-3.12-cpython-debugGitHub Actions
test-conda-python-3.13GitHub Actions
test-conda-python-3.9GitHub Actions
test-conda-python-3.9-pandas-1.1.3-numpy-1.19.5GitHub Actions
test-conda-python-emscriptenGitHub Actions
test-cuda-python-ubuntu-22.04-cuda-11.7.1GitHub Actions
test-debian-12-python-3-amd64GitHub Actions
test-debian-12-python-3-i386GitHub Actions
test-fedora-39-python-3GitHub Actions
test-ubuntu-22.04-python-3GitHub Actions
test-ubuntu-22.04-python-313-freethreadingGitHub Actions
test-ubuntu-24.04-python-3GitHub Actions

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Feb 20, 2025

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, will merge if CI is green.

@pitrou

Copy link
Copy Markdown
Member

CI failures are unrelated.

@Linchin

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

@omatthew98

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

We (Ray Data team) are also running into backward compatibility issues like this in our tests against pyarrow nightly with the same error mentioned here:

[2025-02-25T06:27:20Z] ===================================FAILURES===================================--| [2025-02-25T06:27:20Z] ____________test_convert_to_pyarrow_array_object_ext_type_fallback____________| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] deftest_convert_to_pyarrow_array_object_ext_type_fallback():
| [2025-02-25T06:27:20Z] column_values=create_ragged_ndarray(
| [2025-02-25T06:27:20Z] [
| [2025-02-25T06:27:20Z] "hi",
| [2025-02-25T06:27:20Z] 1,
| [2025-02-25T06:27:20Z] None,
| [2025-02-25T06:27:20Z] [[[[]]]],
| [2025-02-25T06:27:20Z] {"a": [[{"b": 2, "c": UserObj(i=123)}]]},
| [2025-02-25T06:27:20Z] UserObj(i=456),
| [2025-02-25T06:27:20Z] ]
| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z] column_name="py_object_column"| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # First, assert that straightforward conversion into Arrow native types fails| [2025-02-25T06:27:20Z] withpytest.raises(ArrowConversionError) asexc_info:
| [2025-02-25T06:27:20Z] _convert_to_pyarrow_native_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] assert (
| [2025-02-25T06:27:20Z] str(exc_info.value)
| [2025-02-25T06:27:20Z] =="Error converting data to Arrow: ['hi' 1 None list([[[[]]]]) {'a': [[{'b': 2, 'c': UserObj(i=123)}]]}\n UserObj(i=456)]"# noqa: E501| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # Subsequently, assert that fallback to `ArrowObjectExtensionType` succeeds| [2025-02-25T06:27:20Z] pa_array=convert_to_pyarrow_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] >assertpa_array.to_pylist() ==column_values.tolist()
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] python/ray/air/tests/test_arrow.py:121:
| [2025-02-25T06:27:20Z] _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] > ???
| [2025-02-25T06:27:20Z] ETypeError: as_py() gotanunexpectedkeywordargument'maps_as_pydicts'

@pitrou

Copy link
Copy Markdown
Member

@Linchin@omatthew98 I think the way around this would be to take a **kwargs in your as_py method and then forward it to any nested as_py call (if any).

For example turn this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returnJSONArray._deserialize_json(self.value.as_py() ifself.valueelseNone)

into this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returnJSONArray._deserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

@pitrou

Copy link
Copy Markdown
Member

I've updated the PR description, we should remember to call out this potential incompatibility in the release notes for the next version.

raulchen pushed a commit to ray-project/ray that referenced this pull request Mar 3, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
xsuler pushed a commit to antgroup/ant-ray that referenced this pull request Mar 4, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
abrarsheikh pushed a commit to ray-project/ray that referenced this pull request Mar 8, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Signed-off-by: Abrar Sheikh <abrar@anyscale.com>
park12sj pushed a commit to park12sj/ray that referenced this pull request Mar 18, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jonasdedden@pitrou@Linchin@omatthew98
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py - #45471

Merged
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter
Feb 20, 2025
Merged

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py#45471
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter

Conversation

@jonasdedden

@jonasdeddenjonasdedden commented Feb 9, 2025

Copy link
Copy Markdown
Contributor

Rationale for this change

Currently, unfortunately MapScalar/Array types are not deserialized into proper Python dicts, which is unfortunate since this breaks "roundtrips" from Python -> Arrow -> Python:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
# [{'x': [('a', 1)]}]

This is especially bad when storing TiBs of deeply nested data (think of lists in structs in maps...) that were created from Python and serialized into Arrow/Parquet, since they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds.

What changes are included in this PR?

A new parameter maps_as_pydicts is introduced to to_pylist, to_pydict, as_py which will allow proper roundtrips:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist(maps_as_pydicts="strict")
# [{'x': {'a': 1}}]

Are these changes tested?

Yes. There are tests for to_pylist and to_pydict included for pyarrow.Table, whilst low-level MapScalar and especially a nesting with ListScalar and StructScalar is tested.

Also, duplicate keys now should throw an error, which is also tested for.

Are there any user-facing changes?

Yes. The as_py() method on Scalar instances can be called with a new keyword argument maps_as_pydicts.

As a consequence, if you implement your own Scalar subclass (for example for an extension type), you should change its signature to accept that new argument. For example this definition:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returndeserialize_json(self.value.as_py() ifself.valueelseNone)

could be changed to:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returndeserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

Fix ExampleUuidScalarType
Add tests for `maps_as_pydicts`
Add test for duplicate map keys
Formatting fixes
Add docstring for 'maps_as_pydicts'
Formatting fixes
Call from_arrays from Table
Fix last hopefully issues
Correct MapScalar method "as_py" when there are multiple keys present
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #39010has been automatically assigned in GitHub to PR creator.

@pitrou

Copy link
Copy Markdown
Member

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

@pitrou

Copy link
Copy Markdown
Member

Also:

they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds

Please note that from_pylist and to_pylist are quite costly in themselves. Usually you want to avoid these kinds of roundtrips to/from Python objects if you are concerned with performance.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

Let me clarify what this is about. Map fields are already createable with from_pylist by using list of tuples, as I show in the tests I added. Even the code in my initial message can show this. Fundamentally, it's about adding opt-in behaviour to to_pylist to arrive at a functionality one would expect from a Python perspective:

data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
^---------------------------------------------^
this works fine, data will properly encoded in the Arrow way of encoding Maps
^---------^
this will give lists of tuples instead of dicts 

You can use data = [{'x': [('a', 1)]}] here too, this will yield the same RecordBatch. This then of course technically would qualify as a proper "roundtrip", but this is not what this issue is about, it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

Please note that from_pylist and to_pylist are quite costly in themselves.

Yes, but this is part of a very large distributed machine learning setup, where relatively intricate filters applied on deeply nested list/struct/map columns. The compute of the actual machine learning outclasses the compute one has to do to deserialize Python objects by many orders of magnitude.

For pure data queries, we would not use bare Python objects of course.

@pitrou

Copy link
Copy Markdown
Member

it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

I see, thanks. Then, do we want to reuse the same parameter signature as in the Pandas-related PR? I.e., allow either None, "lossy" and "strict", rather than a boolean.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

allow either None, "lossy" and "strict", rather than a boolean.

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method? This has to be done because the to_pylist method calls as_py on its member arrays (which can be all possible types), and therefore all array/scalar types have to support this parameter. I did not see any other way to easily implement this. I'm willing to do quick progress here, so if you come up with another idea, let me know.

@pitrou

Copy link
Copy Markdown
Member

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method?

That sounds ok to me. Ideally, to_pylist wouldn't call as_py in a loop (which is going to be quite slow), but that would be a major refactor.

@pitrou

Copy link
Copy Markdown
Member

By the way, we probably want to make the new parameter keyword-only?

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I addressed the remarks :) There is some weird error in the "Docs" job, I don't know what this is about.

@pitrou

Copy link
Copy Markdown
Member

Hmm, it looks like some of the CI failures will need #45500 to be merged first

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I rebased the branch, now the CI tests seem fine again, I think?

Could we get a approval/review of this? :)

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jonded94 ! This looks good on the principle, here are some assorted comments.

Comment threadpython/pyarrow/array.pxi Outdated
Comment on lines +1667 to +1668
This can change the ordering of (key, value) pairs, and will
deduplicate multiple keys, resulting in a possible loss of data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the ordering comment is obsolete, as Python dicts are ordered nowadays. Unless the underlying implementation does something weird, ordering should therefore be preserved.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the ordering part, added some explanation of which value survives on duplicate keys.

Comment threadpython/pyarrow/array.pxi
Comment threadpython/pyarrow/table.pxi Outdated
Arrow Map, as in [(key1, value1), (key2, value2), ...].

If 'lossy' or 'strict', convert Arrow Map arrays to native Python dicts.
This can change the ordering of (key, value) pairs, and will

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: ordering

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Comment threadpython/pyarrow/tests/test_scalars.py Outdated
with pytest.raises(ValueError):
assert s.as_py(maps_as_pydicts="strict")

assert s.as_py(maps_as_pydicts="lossy") == {'a': 2}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check that a warning is actually emitted? See pytest.warns

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented a check for this warning

Comment threadpython/pyarrow/scalar.pxi
Comment threadpython/pyarrow/scalar.pxi Outdated
raise ValueError(
"Invalid value for 'maps_as_pydicts': "
+ "valid values are 'lossy', 'strict' or `None` (default). "
+ f"Received '{maps_as_pydicts}'."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it may be more idiomatic to use the repr here

Suggested change
+ f"Received '{maps_as_pydicts}'."
+ f"Received {maps_as_pydicts!r}."

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the suggested change

Comment threadpython/pyarrow/scalar.pxi Outdated
for key, value in self:
if key in result_dict:
if maps_as_pydicts == "strict":
raise ValueError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this a KeyError. Also, the message should perhaps contain the duplicate key?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a KeyError

@pitrou

Copy link
Copy Markdown
Member

@github-actions crossbow submit -g python

@github-actions

Copy link
Copy Markdown

Revision: 93045c4

Submitted crossbow builds: ursacomputing/crossbow @ actions-9728f80818

TaskStatus
example-python-minimal-build-fedora-condaGitHub Actions
example-python-minimal-build-ubuntu-venvGitHub Actions
test-conda-python-3.10GitHub Actions
test-conda-python-3.10-hdfs-2.9.2GitHub Actions
test-conda-python-3.10-hdfs-3.2.1GitHub Actions
test-conda-python-3.10-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11GitHub Actions
test-conda-python-3.11-dask-latestGitHub Actions
test-conda-python-3.11-dask-upstream_develGitHub Actions
test-conda-python-3.11-hypothesisGitHub Actions
test-conda-python-3.11-pandas-latest-numpy-1.26GitHub Actions
test-conda-python-3.11-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11-pandas-nightly-numpy-nightlyGitHub Actions
test-conda-python-3.11-pandas-upstream_devel-numpy-nightlyGitHub Actions
test-conda-python-3.11-spark-masterGitHub Actions
test-conda-python-3.12GitHub Actions
test-conda-python-3.12-cpython-debugGitHub Actions
test-conda-python-3.13GitHub Actions
test-conda-python-3.9GitHub Actions
test-conda-python-3.9-pandas-1.1.3-numpy-1.19.5GitHub Actions
test-conda-python-emscriptenGitHub Actions
test-cuda-python-ubuntu-22.04-cuda-11.7.1GitHub Actions
test-debian-12-python-3-amd64GitHub Actions
test-debian-12-python-3-i386GitHub Actions
test-fedora-39-python-3GitHub Actions
test-ubuntu-22.04-python-3GitHub Actions
test-ubuntu-22.04-python-313-freethreadingGitHub Actions
test-ubuntu-24.04-python-3GitHub Actions

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Feb 20, 2025

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, will merge if CI is green.

@pitrou

Copy link
Copy Markdown
Member

CI failures are unrelated.

@Linchin

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

@omatthew98

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

We (Ray Data team) are also running into backward compatibility issues like this in our tests against pyarrow nightly with the same error mentioned here:

[2025-02-25T06:27:20Z] ===================================FAILURES===================================--| [2025-02-25T06:27:20Z] ____________test_convert_to_pyarrow_array_object_ext_type_fallback____________| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] deftest_convert_to_pyarrow_array_object_ext_type_fallback():
| [2025-02-25T06:27:20Z] column_values=create_ragged_ndarray(
| [2025-02-25T06:27:20Z] [
| [2025-02-25T06:27:20Z] "hi",
| [2025-02-25T06:27:20Z] 1,
| [2025-02-25T06:27:20Z] None,
| [2025-02-25T06:27:20Z] [[[[]]]],
| [2025-02-25T06:27:20Z] {"a": [[{"b": 2, "c": UserObj(i=123)}]]},
| [2025-02-25T06:27:20Z] UserObj(i=456),
| [2025-02-25T06:27:20Z] ]
| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z] column_name="py_object_column"| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # First, assert that straightforward conversion into Arrow native types fails| [2025-02-25T06:27:20Z] withpytest.raises(ArrowConversionError) asexc_info:
| [2025-02-25T06:27:20Z] _convert_to_pyarrow_native_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] assert (
| [2025-02-25T06:27:20Z] str(exc_info.value)
| [2025-02-25T06:27:20Z] =="Error converting data to Arrow: ['hi' 1 None list([[[[]]]]) {'a': [[{'b': 2, 'c': UserObj(i=123)}]]}\n UserObj(i=456)]"# noqa: E501| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # Subsequently, assert that fallback to `ArrowObjectExtensionType` succeeds| [2025-02-25T06:27:20Z] pa_array=convert_to_pyarrow_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] >assertpa_array.to_pylist() ==column_values.tolist()
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] python/ray/air/tests/test_arrow.py:121:
| [2025-02-25T06:27:20Z] _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] > ???
| [2025-02-25T06:27:20Z] ETypeError: as_py() gotanunexpectedkeywordargument'maps_as_pydicts'

@pitrou

Copy link
Copy Markdown
Member

@Linchin@omatthew98 I think the way around this would be to take a **kwargs in your as_py method and then forward it to any nested as_py call (if any).

For example turn this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returnJSONArray._deserialize_json(self.value.as_py() ifself.valueelseNone)

into this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returnJSONArray._deserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

@pitrou

Copy link
Copy Markdown
Member

I've updated the PR description, we should remember to call out this potential incompatibility in the release notes for the next version.

raulchen pushed a commit to ray-project/ray that referenced this pull request Mar 3, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
xsuler pushed a commit to antgroup/ant-ray that referenced this pull request Mar 4, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
abrarsheikh pushed a commit to ray-project/ray that referenced this pull request Mar 8, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Signed-off-by: Abrar Sheikh <abrar@anyscale.com>
park12sj pushed a commit to park12sj/ray that referenced this pull request Mar 18, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jonasdedden@pitrou@Linchin@omatthew98
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py - #45471

Merged
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter
Feb 20, 2025
Merged

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py#45471
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter

Conversation

@jonasdedden

@jonasdeddenjonasdedden commented Feb 9, 2025

Copy link
Copy Markdown
Contributor

Rationale for this change

Currently, unfortunately MapScalar/Array types are not deserialized into proper Python dicts, which is unfortunate since this breaks "roundtrips" from Python -> Arrow -> Python:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
# [{'x': [('a', 1)]}]

This is especially bad when storing TiBs of deeply nested data (think of lists in structs in maps...) that were created from Python and serialized into Arrow/Parquet, since they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds.

What changes are included in this PR?

A new parameter maps_as_pydicts is introduced to to_pylist, to_pydict, as_py which will allow proper roundtrips:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist(maps_as_pydicts="strict")
# [{'x': {'a': 1}}]

Are these changes tested?

Yes. There are tests for to_pylist and to_pydict included for pyarrow.Table, whilst low-level MapScalar and especially a nesting with ListScalar and StructScalar is tested.

Also, duplicate keys now should throw an error, which is also tested for.

Are there any user-facing changes?

Yes. The as_py() method on Scalar instances can be called with a new keyword argument maps_as_pydicts.

As a consequence, if you implement your own Scalar subclass (for example for an extension type), you should change its signature to accept that new argument. For example this definition:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returndeserialize_json(self.value.as_py() ifself.valueelseNone)

could be changed to:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returndeserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

Fix ExampleUuidScalarType
Add tests for `maps_as_pydicts`
Add test for duplicate map keys
Formatting fixes
Add docstring for 'maps_as_pydicts'
Formatting fixes
Call from_arrays from Table
Fix last hopefully issues
Correct MapScalar method "as_py" when there are multiple keys present
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #39010has been automatically assigned in GitHub to PR creator.

@pitrou

Copy link
Copy Markdown
Member

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

@pitrou

Copy link
Copy Markdown
Member

Also:

they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds

Please note that from_pylist and to_pylist are quite costly in themselves. Usually you want to avoid these kinds of roundtrips to/from Python objects if you are concerned with performance.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

Let me clarify what this is about. Map fields are already createable with from_pylist by using list of tuples, as I show in the tests I added. Even the code in my initial message can show this. Fundamentally, it's about adding opt-in behaviour to to_pylist to arrive at a functionality one would expect from a Python perspective:

data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
^---------------------------------------------^
this works fine, data will properly encoded in the Arrow way of encoding Maps
^---------^
this will give lists of tuples instead of dicts 

You can use data = [{'x': [('a', 1)]}] here too, this will yield the same RecordBatch. This then of course technically would qualify as a proper "roundtrip", but this is not what this issue is about, it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

Please note that from_pylist and to_pylist are quite costly in themselves.

Yes, but this is part of a very large distributed machine learning setup, where relatively intricate filters applied on deeply nested list/struct/map columns. The compute of the actual machine learning outclasses the compute one has to do to deserialize Python objects by many orders of magnitude.

For pure data queries, we would not use bare Python objects of course.

@pitrou

Copy link
Copy Markdown
Member

it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

I see, thanks. Then, do we want to reuse the same parameter signature as in the Pandas-related PR? I.e., allow either None, "lossy" and "strict", rather than a boolean.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

allow either None, "lossy" and "strict", rather than a boolean.

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method? This has to be done because the to_pylist method calls as_py on its member arrays (which can be all possible types), and therefore all array/scalar types have to support this parameter. I did not see any other way to easily implement this. I'm willing to do quick progress here, so if you come up with another idea, let me know.

@pitrou

Copy link
Copy Markdown
Member

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method?

That sounds ok to me. Ideally, to_pylist wouldn't call as_py in a loop (which is going to be quite slow), but that would be a major refactor.

@pitrou

Copy link
Copy Markdown
Member

By the way, we probably want to make the new parameter keyword-only?

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I addressed the remarks :) There is some weird error in the "Docs" job, I don't know what this is about.

@pitrou

Copy link
Copy Markdown
Member

Hmm, it looks like some of the CI failures will need #45500 to be merged first

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I rebased the branch, now the CI tests seem fine again, I think?

Could we get a approval/review of this? :)

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jonded94 ! This looks good on the principle, here are some assorted comments.

Comment threadpython/pyarrow/array.pxi Outdated
Comment on lines +1667 to +1668
This can change the ordering of (key, value) pairs, and will
deduplicate multiple keys, resulting in a possible loss of data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the ordering comment is obsolete, as Python dicts are ordered nowadays. Unless the underlying implementation does something weird, ordering should therefore be preserved.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the ordering part, added some explanation of which value survives on duplicate keys.

Comment threadpython/pyarrow/array.pxi
Comment threadpython/pyarrow/table.pxi Outdated
Arrow Map, as in [(key1, value1), (key2, value2), ...].

If 'lossy' or 'strict', convert Arrow Map arrays to native Python dicts.
This can change the ordering of (key, value) pairs, and will

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: ordering

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Comment threadpython/pyarrow/tests/test_scalars.py Outdated
with pytest.raises(ValueError):
assert s.as_py(maps_as_pydicts="strict")

assert s.as_py(maps_as_pydicts="lossy") == {'a': 2}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check that a warning is actually emitted? See pytest.warns

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented a check for this warning

Comment threadpython/pyarrow/scalar.pxi
Comment threadpython/pyarrow/scalar.pxi Outdated
raise ValueError(
"Invalid value for 'maps_as_pydicts': "
+ "valid values are 'lossy', 'strict' or `None` (default). "
+ f"Received '{maps_as_pydicts}'."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it may be more idiomatic to use the repr here

Suggested change
+ f"Received '{maps_as_pydicts}'."
+ f"Received {maps_as_pydicts!r}."

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the suggested change

Comment threadpython/pyarrow/scalar.pxi Outdated
for key, value in self:
if key in result_dict:
if maps_as_pydicts == "strict":
raise ValueError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this a KeyError. Also, the message should perhaps contain the duplicate key?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a KeyError

@pitrou

Copy link
Copy Markdown
Member

@github-actions crossbow submit -g python

@github-actions

Copy link
Copy Markdown

Revision: 93045c4

Submitted crossbow builds: ursacomputing/crossbow @ actions-9728f80818

TaskStatus
example-python-minimal-build-fedora-condaGitHub Actions
example-python-minimal-build-ubuntu-venvGitHub Actions
test-conda-python-3.10GitHub Actions
test-conda-python-3.10-hdfs-2.9.2GitHub Actions
test-conda-python-3.10-hdfs-3.2.1GitHub Actions
test-conda-python-3.10-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11GitHub Actions
test-conda-python-3.11-dask-latestGitHub Actions
test-conda-python-3.11-dask-upstream_develGitHub Actions
test-conda-python-3.11-hypothesisGitHub Actions
test-conda-python-3.11-pandas-latest-numpy-1.26GitHub Actions
test-conda-python-3.11-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11-pandas-nightly-numpy-nightlyGitHub Actions
test-conda-python-3.11-pandas-upstream_devel-numpy-nightlyGitHub Actions
test-conda-python-3.11-spark-masterGitHub Actions
test-conda-python-3.12GitHub Actions
test-conda-python-3.12-cpython-debugGitHub Actions
test-conda-python-3.13GitHub Actions
test-conda-python-3.9GitHub Actions
test-conda-python-3.9-pandas-1.1.3-numpy-1.19.5GitHub Actions
test-conda-python-emscriptenGitHub Actions
test-cuda-python-ubuntu-22.04-cuda-11.7.1GitHub Actions
test-debian-12-python-3-amd64GitHub Actions
test-debian-12-python-3-i386GitHub Actions
test-fedora-39-python-3GitHub Actions
test-ubuntu-22.04-python-3GitHub Actions
test-ubuntu-22.04-python-313-freethreadingGitHub Actions
test-ubuntu-24.04-python-3GitHub Actions

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Feb 20, 2025

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, will merge if CI is green.

@pitrou

Copy link
Copy Markdown
Member

CI failures are unrelated.

@Linchin

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

@omatthew98

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

We (Ray Data team) are also running into backward compatibility issues like this in our tests against pyarrow nightly with the same error mentioned here:

[2025-02-25T06:27:20Z] ===================================FAILURES===================================--| [2025-02-25T06:27:20Z] ____________test_convert_to_pyarrow_array_object_ext_type_fallback____________| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] deftest_convert_to_pyarrow_array_object_ext_type_fallback():
| [2025-02-25T06:27:20Z] column_values=create_ragged_ndarray(
| [2025-02-25T06:27:20Z] [
| [2025-02-25T06:27:20Z] "hi",
| [2025-02-25T06:27:20Z] 1,
| [2025-02-25T06:27:20Z] None,
| [2025-02-25T06:27:20Z] [[[[]]]],
| [2025-02-25T06:27:20Z] {"a": [[{"b": 2, "c": UserObj(i=123)}]]},
| [2025-02-25T06:27:20Z] UserObj(i=456),
| [2025-02-25T06:27:20Z] ]
| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z] column_name="py_object_column"| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # First, assert that straightforward conversion into Arrow native types fails| [2025-02-25T06:27:20Z] withpytest.raises(ArrowConversionError) asexc_info:
| [2025-02-25T06:27:20Z] _convert_to_pyarrow_native_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] assert (
| [2025-02-25T06:27:20Z] str(exc_info.value)
| [2025-02-25T06:27:20Z] =="Error converting data to Arrow: ['hi' 1 None list([[[[]]]]) {'a': [[{'b': 2, 'c': UserObj(i=123)}]]}\n UserObj(i=456)]"# noqa: E501| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # Subsequently, assert that fallback to `ArrowObjectExtensionType` succeeds| [2025-02-25T06:27:20Z] pa_array=convert_to_pyarrow_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] >assertpa_array.to_pylist() ==column_values.tolist()
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] python/ray/air/tests/test_arrow.py:121:
| [2025-02-25T06:27:20Z] _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] > ???
| [2025-02-25T06:27:20Z] ETypeError: as_py() gotanunexpectedkeywordargument'maps_as_pydicts'

@pitrou

Copy link
Copy Markdown
Member

@Linchin@omatthew98 I think the way around this would be to take a **kwargs in your as_py method and then forward it to any nested as_py call (if any).

For example turn this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returnJSONArray._deserialize_json(self.value.as_py() ifself.valueelseNone)

into this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returnJSONArray._deserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

@pitrou

Copy link
Copy Markdown
Member

I've updated the PR description, we should remember to call out this potential incompatibility in the release notes for the next version.

raulchen pushed a commit to ray-project/ray that referenced this pull request Mar 3, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
xsuler pushed a commit to antgroup/ant-ray that referenced this pull request Mar 4, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
abrarsheikh pushed a commit to ray-project/ray that referenced this pull request Mar 8, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Signed-off-by: Abrar Sheikh <abrar@anyscale.com>
park12sj pushed a commit to park12sj/ray that referenced this pull request Mar 18, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jonasdedden@pitrou@Linchin@omatthew98
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py - #45471

Merged
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter
Feb 20, 2025
Merged

GH-39010: [Python] Introduce maps_as_pydicts parameter for to_pylist, to_pydict, as_py#45471
pitrou merged 10 commits into
apache:mainfrom
jonasdedden:introduce-maps-as-pydicts-parameter

Conversation

@jonasdedden

@jonasdeddenjonasdedden commented Feb 9, 2025

Copy link
Copy Markdown
Contributor

Rationale for this change

Currently, unfortunately MapScalar/Array types are not deserialized into proper Python dicts, which is unfortunate since this breaks "roundtrips" from Python -> Arrow -> Python:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
# [{'x': [('a', 1)]}]

This is especially bad when storing TiBs of deeply nested data (think of lists in structs in maps...) that were created from Python and serialized into Arrow/Parquet, since they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds.

What changes are included in this PR?

A new parameter maps_as_pydicts is introduced to to_pylist, to_pydict, as_py which will allow proper roundtrips:

import pyarrow as pa
schema = pa.schema([pa.field('x', pa.map_(pa.string(), pa.int64()))])
data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist(maps_as_pydicts="strict")
# [{'x': {'a': 1}}]

Are these changes tested?

Yes. There are tests for to_pylist and to_pydict included for pyarrow.Table, whilst low-level MapScalar and especially a nesting with ListScalar and StructScalar is tested.

Also, duplicate keys now should throw an error, which is also tested for.

Are there any user-facing changes?

Yes. The as_py() method on Scalar instances can be called with a new keyword argument maps_as_pydicts.

As a consequence, if you implement your own Scalar subclass (for example for an extension type), you should change its signature to accept that new argument. For example this definition:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returndeserialize_json(self.value.as_py() ifself.valueelseNone)

could be changed to:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returndeserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

Fix ExampleUuidScalarType
Add tests for `maps_as_pydicts`
Add test for duplicate map keys
Formatting fixes
Add docstring for 'maps_as_pydicts'
Formatting fixes
Call from_arrays from Table
Fix last hopefully issues
Correct MapScalar method "as_py" when there are multiple keys present
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #39010has been automatically assigned in GitHub to PR creator.

@pitrou

Copy link
Copy Markdown
Member

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

@pitrou

Copy link
Copy Markdown
Member

Also:

they can't be read in again with native pyarrow methods without doing extremely ugly and computationally costly workarounds

Please note that from_pylist and to_pylist are quite costly in themselves. Usually you want to avoid these kinds of roundtrips to/from Python objects if you are concerned with performance.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

While this is not a bad idea in itself, it seems like the roundtripping concern could be solved more efficiently by making from_pylist accept a list of tuples for map fields.

Let me clarify what this is about. Map fields are already createable with from_pylist by using list of tuples, as I show in the tests I added. Even the code in my initial message can show this. Fundamentally, it's about adding opt-in behaviour to to_pylist to arrive at a functionality one would expect from a Python perspective:

data = [{'x': {'a': 1}}]
pa.RecordBatch.from_pylist(data, schema=schema).to_pylist()
^---------------------------------------------^
this works fine, data will properly encoded in the Arrow way of encoding Maps
^---------^
this will give lists of tuples instead of dicts 

You can use data = [{'x': [('a', 1)]}] here too, this will yield the same RecordBatch. This then of course technically would qualify as a proper "roundtrip", but this is not what this issue is about, it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

Please note that from_pylist and to_pylist are quite costly in themselves.

Yes, but this is part of a very large distributed machine learning setup, where relatively intricate filters applied on deeply nested list/struct/map columns. The compute of the actual machine learning outclasses the compute one has to do to deserialize Python objects by many orders of magnitude.

For pure data queries, we would not use bare Python objects of course.

@pitrou

Copy link
Copy Markdown
Member

it's about deserializing Map Arrow types as the ~"expected" Python equivalent, at least as an opt-in method such as pandas already supports for some couple of years now (shown in the linked Github issue).

I see, thanks. Then, do we want to reuse the same parameter signature as in the Pandas-related PR? I.e., allow either None, "lossy" and "strict", rather than a boolean.

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

allow either None, "lossy" and "strict", rather than a boolean.

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method? This has to be done because the to_pylist method calls as_py on its member arrays (which can be all possible types), and therefore all array/scalar types have to support this parameter. I did not see any other way to easily implement this. I'm willing to do quick progress here, so if you come up with another idea, let me know.

@pitrou

Copy link
Copy Markdown
Member

Sure, I actually also stumbled across that when I revisited that original Github issue. Before I do that, I'd like to ask whether you're generally fine with adding this new parameter to everyas_py method?

That sounds ok to me. Ideally, to_pylist wouldn't call as_py in a loop (which is going to be quite slow), but that would be a major refactor.

@pitrou

Copy link
Copy Markdown
Member

By the way, we probably want to make the new parameter keyword-only?

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I addressed the remarks :) There is some weird error in the "Docs" job, I don't know what this is about.

@pitrou

Copy link
Copy Markdown
Member

Hmm, it looks like some of the CI failures will need #45500 to be merged first

@jonasdedden

Copy link
Copy Markdown
ContributorAuthor

I rebased the branch, now the CI tests seem fine again, I think?

Could we get a approval/review of this? :)

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jonded94 ! This looks good on the principle, here are some assorted comments.

Comment threadpython/pyarrow/array.pxi Outdated
Comment on lines +1667 to +1668
This can change the ordering of (key, value) pairs, and will
deduplicate multiple keys, resulting in a possible loss of data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the ordering comment is obsolete, as Python dicts are ordered nowadays. Unless the underlying implementation does something weird, ordering should therefore be preserved.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the ordering part, added some explanation of which value survives on duplicate keys.

Comment threadpython/pyarrow/array.pxi
Comment threadpython/pyarrow/table.pxi Outdated
Arrow Map, as in [(key1, value1), (key2, value2), ...].

If 'lossy' or 'strict', convert Arrow Map arrays to native Python dicts.
This can change the ordering of (key, value) pairs, and will

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment re: ordering

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Comment threadpython/pyarrow/tests/test_scalars.py Outdated
with pytest.raises(ValueError):
assert s.as_py(maps_as_pydicts="strict")

assert s.as_py(maps_as_pydicts="lossy") == {'a': 2}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check that a warning is actually emitted? See pytest.warns

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented a check for this warning

Comment threadpython/pyarrow/scalar.pxi
Comment threadpython/pyarrow/scalar.pxi Outdated
raise ValueError(
"Invalid value for 'maps_as_pydicts': "
+ "valid values are 'lossy', 'strict' or `None` (default). "
+ f"Received '{maps_as_pydicts}'."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it may be more idiomatic to use the repr here

Suggested change
+ f"Received '{maps_as_pydicts}'."
+ f"Received {maps_as_pydicts!r}."

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented the suggested change

Comment threadpython/pyarrow/scalar.pxi Outdated
for key, value in self:
if key in result_dict:
if maps_as_pydicts == "strict":
raise ValueError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make this a KeyError. Also, the message should perhaps contain the duplicate key?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made it a KeyError

@pitrou

Copy link
Copy Markdown
Member

@github-actions crossbow submit -g python

@github-actions

Copy link
Copy Markdown

Revision: 93045c4

Submitted crossbow builds: ursacomputing/crossbow @ actions-9728f80818

TaskStatus
example-python-minimal-build-fedora-condaGitHub Actions
example-python-minimal-build-ubuntu-venvGitHub Actions
test-conda-python-3.10GitHub Actions
test-conda-python-3.10-hdfs-2.9.2GitHub Actions
test-conda-python-3.10-hdfs-3.2.1GitHub Actions
test-conda-python-3.10-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11GitHub Actions
test-conda-python-3.11-dask-latestGitHub Actions
test-conda-python-3.11-dask-upstream_develGitHub Actions
test-conda-python-3.11-hypothesisGitHub Actions
test-conda-python-3.11-pandas-latest-numpy-1.26GitHub Actions
test-conda-python-3.11-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.11-pandas-nightly-numpy-nightlyGitHub Actions
test-conda-python-3.11-pandas-upstream_devel-numpy-nightlyGitHub Actions
test-conda-python-3.11-spark-masterGitHub Actions
test-conda-python-3.12GitHub Actions
test-conda-python-3.12-cpython-debugGitHub Actions
test-conda-python-3.13GitHub Actions
test-conda-python-3.9GitHub Actions
test-conda-python-3.9-pandas-1.1.3-numpy-1.19.5GitHub Actions
test-conda-python-emscriptenGitHub Actions
test-cuda-python-ubuntu-22.04-cuda-11.7.1GitHub Actions
test-debian-12-python-3-amd64GitHub Actions
test-debian-12-python-3-i386GitHub Actions
test-fedora-39-python-3GitHub Actions
test-ubuntu-22.04-python-3GitHub Actions
test-ubuntu-22.04-python-313-freethreadingGitHub Actions
test-ubuntu-24.04-python-3GitHub Actions

@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Feb 20, 2025

@pitroupitrou left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, will merge if CI is green.

@pitrou

Copy link
Copy Markdown
Member

CI failures are unrelated.

@Linchin

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

@omatthew98

Copy link
Copy Markdown

Just fyi this might cause backward incompatibility issue because the user defined extension types are not expecting maps_as_pydicts as an argument for to_py(). We are seeing this in our prerelease tests:

_________________________ test_json_arrow_record_batch _________________________
def test_json_arrow_record_batch():
data = [
json.dumps(value, sort_keys=True, separators=(",", ":"))
for value in JSON_DATA.values()
]
arr = pa.array(data, type=db_dtypes.JSONArrowType())
batch = pa.RecordBatch.from_arrays([arr], ["json_col"])
sink = pa.BufferOutputStream()
with pa.RecordBatchStreamWriter(sink, batch.schema) as writer:
writer.write_batch(batch)
buf = sink.getvalue()
with pa.ipc.open_stream(buf) as reader:
result = reader.read_all()
json_col = result.column("json_col")
assert isinstance(json_col.type, db_dtypes.JSONArrowType)
> s = json_col.to_pylist()
tests/unit/test_json.py:225: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ pyarrow/table.pxi:1380: in pyarrow.lib.ChunkedArray.to_pylist
???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ???
E TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
pyarrow/array.pxi:1[67](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:68)7: TypeError
- generated xml file: /home/runner/work/python-db-dtypes-pandas/python-db-dtypes-pandas/unit_prerelease_3.12_sponge_log.xml -
=========================== short test summary info ============================
FAILED tests/unit/test_json.py::test_json_arrow_to_pylist - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
FAILED tests/unit/test_json.py::test_json_arrow_record_batch - TypeError: JSONArrowScalar.as_py() got an unexpected keyword argument 'maps_as_pydicts'
2 failed, 298 passed in 1.[81](https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310#step:5:82)s

(Link: https://github.com/googleapis/python-db-dtypes-pandas/actions/runs/13017353125/job/37736481135?pr=310)

We (Ray Data team) are also running into backward compatibility issues like this in our tests against pyarrow nightly with the same error mentioned here:

[2025-02-25T06:27:20Z] ===================================FAILURES===================================--| [2025-02-25T06:27:20Z] ____________test_convert_to_pyarrow_array_object_ext_type_fallback____________| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] deftest_convert_to_pyarrow_array_object_ext_type_fallback():
| [2025-02-25T06:27:20Z] column_values=create_ragged_ndarray(
| [2025-02-25T06:27:20Z] [
| [2025-02-25T06:27:20Z] "hi",
| [2025-02-25T06:27:20Z] 1,
| [2025-02-25T06:27:20Z] None,
| [2025-02-25T06:27:20Z] [[[[]]]],
| [2025-02-25T06:27:20Z] {"a": [[{"b": 2, "c": UserObj(i=123)}]]},
| [2025-02-25T06:27:20Z] UserObj(i=456),
| [2025-02-25T06:27:20Z] ]
| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z] column_name="py_object_column"| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # First, assert that straightforward conversion into Arrow native types fails| [2025-02-25T06:27:20Z] withpytest.raises(ArrowConversionError) asexc_info:
| [2025-02-25T06:27:20Z] _convert_to_pyarrow_native_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] assert (
| [2025-02-25T06:27:20Z] str(exc_info.value)
| [2025-02-25T06:27:20Z] =="Error converting data to Arrow: ['hi' 1 None list([[[[]]]]) {'a': [[{'b': 2, 'c': UserObj(i=123)}]]}\n UserObj(i=456)]"# noqa: E501| [2025-02-25T06:27:20Z] )
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] # Subsequently, assert that fallback to `ArrowObjectExtensionType` succeeds| [2025-02-25T06:27:20Z] pa_array=convert_to_pyarrow_array(column_values, column_name)
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] >assertpa_array.to_pylist() ==column_values.tolist()
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] python/ray/air/tests/test_arrow.py:121:
| [2025-02-25T06:27:20Z] _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
| [2025-02-25T06:27:20Z]
| [2025-02-25T06:27:20Z] > ???
| [2025-02-25T06:27:20Z] ETypeError: as_py() gotanunexpectedkeywordargument'maps_as_pydicts'

@pitrou

Copy link
Copy Markdown
Member

@Linchin@omatthew98 I think the way around this would be to take a **kwargs in your as_py method and then forward it to any nested as_py call (if any).

For example turn this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self):
returnJSONArray._deserialize_json(self.value.as_py() ifself.valueelseNone)

into this:

classJSONArrowScalar(pa.ExtensionScalar):
defas_py(self, **kwargs):
returnJSONArray._deserialize_json(self.value.as_py(**kwargs) ifself.valueelseNone)

@pitrou

Copy link
Copy Markdown
Member

I've updated the PR description, we should remember to call out this potential incompatibility in the release notes for the next version.

raulchen pushed a commit to ray-project/ray that referenced this pull request Mar 3, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
xsuler pushed a commit to antgroup/ant-ray that referenced this pull request Mar 4, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
abrarsheikh pushed a commit to ray-project/ray that referenced this pull request Mar 8, 2025
#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Signed-off-by: Abrar Sheikh <abrar@anyscale.com>
park12sj pushed a commit to park12sj/ray that referenced this pull request Mar 18, 2025
ray-project#51041)
## Why are these changes needed?
Our tests with pyarrow nightly caught a backwards incompatibility bug
with a [recent pyarrow
change](apache/arrow#45471). To fix this we
simply need to pass along kwargs in our `as_py` method as suggested by
the pyarrow team
[here](apache/arrow#45471 (comment)).
---------
Signed-off-by: Matthew Owen <mowen@anyscale.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@jonasdedden@pitrou@Linchin@omatthew98