Skip to content

GH-47380: [Python] Apply maps_as_pydicts to Nested MapScalar Values - #47454

Merged
raulcd merged 5 commits into
apache:mainfrom
jo-migo:fix-nested-maps-as-pydicts
Oct 7, 2025
Merged

GH-47380: [Python] Apply maps_as_pydicts to Nested MapScalar Values#47454
raulcd merged 5 commits into
apache:mainfrom
jo-migo:fix-nested-maps-as-pydicts

Conversation

@jo-migo

@jo-migojo-migo commented Aug 28, 2025

Copy link
Copy Markdown
Contributor

Rationale for this change

Currently, the maps_as_pydicts parameter to MapScalar.as_py does not work on nested maps. See below:

import pyarrow as pa
t = pa.struct([pa.field("x", pa.map_(pa.string(), pa.map_(pa.string(), pa.int8())))])
v = {"x": {"a": {"1": 1}}}
s = pa.scalar(v, type=t)
print(s.as_py(maps_as_pydicts="strict"))
# {'x': {'a': [('1', 1)]}}

In this ^ case, I'd want to get the value: {'x': {'a': {'1': 1}}}, such that round trips would work as expected.

What changes are included in this PR?

Begin to apply the maps_as_pydicts to nested values in map types as well, update relevant test.

Are these changes tested?

Yes

Are there any user-facing changes?

Yes, just a user-facing fix.

@github-actions

Copy link
Copy Markdown

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

@github-actions

Copy link
Copy Markdown

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

1 similar comment
@github-actions

Copy link
Copy Markdown

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

@AlenkaFAlenkaF 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 for proposing this update!

Supporting map-to-dict conversion in nested cases is definitely useful. The overall change looks good to me — just one suggestion: consider simplifying the iterator used in the zip function, if possible.

Comment threadpython/pyarrow/scalar.pxi Outdated
Comment on lines +1176 to +1177
for k, v in zip(self.values.field(self.type.key_field.name), self.values.field(self.type.item_field.name)):
key = k.as_py()

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 we can simplify this a bit:

Suggested change
for k, v inzip(self.values.field(self.type.key_field.name), self.values.field(self.type.item_field.name)):
key = k.as_py()
for key, value inzip(self.keys(), self.values.field(self.type.item_field.name)):

and maybe something for the second iterator?

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.

Thanks for the suggestion! I had indeed missed the existence of the keys method for simplifying this. Unfortunately I don't see any similar thing for directly iterating over the value fields inside the inner array, so I think that can't be simplified the same way.

@jo-migo
jo-migoforce-pushed the fix-nested-maps-as-pydicts branch from f998961 to 60b963cCompareSeptember 10, 2025 13:12
@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Sep 10, 2025

@AlenkaFAlenkaF 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!
Will wait for another review and then we can merge.

cc @raulcd@rok

@AlenkaF

Copy link
Copy Markdown
Member

@github-actions crossbow submit -g python

@github-actions

Copy link
Copy Markdown

Revision: 60b963c

Submitted crossbow builds: ursacomputing/crossbow @ actions-07e734916d

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-1.3.4-numpy-1.21.2GitHub 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-latestGitHub 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.12-pandas-latest-numpy-1.26GitHub Actions
test-conda-python-3.12-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.13GitHub Actions
test-conda-python-3.13-pandas-nightly-numpy-nightlyGitHub Actions
test-conda-python-3.13-pandas-upstream_devel-numpy-nightlyGitHub 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-42-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

Comment on lines -960 to -963
ty = pa.struct([
pa.field('x', pa.map_(pa.string(), pa.int8())),
pa.field('y', pa.list_(pa.map_(pa.string(), pa.int8()))),
])

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.

could we have both test example validations? We would validate the previous functionality is still working as it was before maintaining the old test.

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.

Sure, I restored the original test and tested this functionality in a new one.

@github-actionsgithub-actionsBot added awaiting changes Awaiting changes and removed awaiting committer review Awaiting committer review labels Sep 18, 2025
@github-actionsgithub-actionsBot removed the awaiting changes Awaiting changes label Sep 22, 2025
@github-actionsgithub-actionsBot added the awaiting change review Awaiting change review label Sep 22, 2025
@jo-migo
jo-migo requested a review from raulcdSeptember 23, 2025 07:20
@jo-migo

Copy link
Copy Markdown
ContributorAuthor

@raulcd@rok could I get another review please?

@jonasdedden

Copy link
Copy Markdown
Contributor

@raulcd@rok just another gentle request for a review? 🥺

@raulcdraulcd 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.

Sorry, it has taken some time to review. I've been quite busy.
Thanks for the fix and for the tests!
The change looks good but I have a nit question, I understand the recursive call for value.as_py but I am unsure I understand why what the issue with the previous iteration, maybe you could explain why the change is needed, potentially add a comment?
Thanks and sorry for taking some time.

return list(self)
result_dict = {}
for key, value in self:
for key, value in zip(self.keys(), self.values.field(self.type.item_field.name)):

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.

What was the issue with the previous iterator?
I am unsure I understand what was wrong with the previous iteration and what are we fixing here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not the author of the MR, but the previous for key, value in self would result in a call of self.__iter__().

That thing is defined above this function, and does yield (k.as_py(), v.as_py()) directly. So it's hardcoded to the default maps_as_pydicts behaviour, which is incompatible to what we want here. The values, if they happen to be map types, basically would be yielded in the non-dict way (as the list of (key, value) tuples).

It's also not possible to adjust the __iter__() function because by definition it has to have no parameters, so it has to be opinionated in some sense about how to handle maps.

So in this case, we have to loop over the keys and values manually and then do the as_py() call on the value type with the correct maps_as_pydicts parameter ourselves.

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 see, that makes sense, thanks for the clarification, should we validate self.values is not None as we currently do on __iter__?

arr=self.valuesifarrisNone:
returnfork, vinzip(arr.field(self.type.key_field.name), arr.field(self.type.item_field.name)):
yield (k.as_py(), v.as_py())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess that would make sense, also adding a test for whether an empty map works could make sense? @jo-migo ?

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.

Sure, I have added an explicit check for self.values is None and corresponding test.

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.

As for the context, check out the PR description for a (hopefully) straightforward minimal example, but it's just like @jonded94 says. The problem with the current implementation is that round trips from python -> arrow -> python are broken at the moment for nested map columns ---

basically we want

{'x': {'a': {'1': 1}}} -> {'x': {'a': {'1': 1}}}

but the current behaviour with maps_as_pydicts is:

{'x': {'a': {'1': 1}}} -> {'x': {'a': [('1', 1)]}}

@github-actionsgithub-actionsBot added awaiting changes Awaiting changes and removed awaiting change review Awaiting change review labels Oct 6, 2025
@github-actionsgithub-actionsBot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Oct 6, 2025
@jo-migo
jo-migoforce-pushed the fix-nested-maps-as-pydicts branch from 384420d to 985c315CompareOctober 6, 2025 14:27
@raulcd

Copy link
Copy Markdown
Member

@github-actions crossbow submit -g python

@raulcdraulcd 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.

Looks good to me, I've triggered CI, I will wait for it to finish. I'll merge afterwards.

@github-actionsgithub-actionsBot added awaiting merge Awaiting merge and removed awaiting change review Awaiting change review labels Oct 6, 2025
@github-actions

Copy link
Copy Markdown

Revision: 45b5431

Submitted crossbow builds: ursacomputing/crossbow @ actions-dda8c557ef

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-1.3.4-numpy-1.21.2GitHub 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-latestGitHub 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.12-pandas-latest-numpy-1.26GitHub Actions
test-conda-python-3.12-pandas-latest-numpy-latestGitHub Actions
test-conda-python-3.13GitHub Actions
test-conda-python-3.13-pandas-nightly-numpy-nightlyGitHub Actions
test-conda-python-3.13-pandas-upstream_devel-numpy-nightlyGitHub 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-42-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

@raulcd
raulcd merged commit 0d32975 into apache:mainOct 7, 2025
16 checks passed
@raulcdraulcd removed the awaiting merge Awaiting merge label Oct 7, 2025
@raulcd

raulcd commented Oct 7, 2025

Copy link
Copy Markdown
Member

None of the CI failures are related to the change.

@conbench-apache-arrow

Copy link
Copy Markdown

After merging your PR, Conbench analyzed the 2 benchmarking runs that have been run so far on merge-commit 0d32975.

There were no benchmark performance regressions. 🎉

The full Conbench report has more details. It also includes information about 22 possible false positives for unstable benchmarks that are known to sometimes produce them.

zanmato1984 pushed a commit to zanmato1984/arrow that referenced this pull request Oct 15, 2025
…lues (apache#47454)
### Rationale for this change
Currently, the `maps_as_pydicts` parameter to `MapScalar.as_py` does not work on nested maps. See below:
```
import pyarrow as pa
t = pa.struct([pa.field("x", pa.map_(pa.string(), pa.map_(pa.string(), pa.int8())))])
v = {"x": {"a": {"1": 1}}}
s = pa.scalar(v, type=t)
print(s.as_py(maps_as_pydicts="strict"))
# {'x': {'a': [('1', 1)]}}
```
In this ^ case, I'd want to get the value: `{'x': {'a': {'1': 1}}}`, such that round trips would work as expected.
### What changes are included in this PR?
Begin to apply the `maps_as_pydicts` to nested values in map types as well, update relevant test.
### Are these changes tested?
Yes
### Are there any user-facing changes?
Yes, just a user-facing fix.
* GitHub Issue: apache#47380
Lead-authored-by: Johanna <johanna.goergen@gmail.com>
Co-authored-by: zzkv <johanna.goergen@gmail.com>
Co-authored-by: Johanna <johanna.goergen@optimizely.com>
Signed-off-by: Raúl Cumplido <raulcumplido@gmail.com>
Mottl pushed a commit to Mottl/arrow that referenced this pull request May 26, 2026
…lues (apache#47454)
### Rationale for this change
Currently, the `maps_as_pydicts` parameter to `MapScalar.as_py` does not work on nested maps. See below:
```
import pyarrow as pa
t = pa.struct([pa.field("x", pa.map_(pa.string(), pa.map_(pa.string(), pa.int8())))])
v = {"x": {"a": {"1": 1}}}
s = pa.scalar(v, type=t)
print(s.as_py(maps_as_pydicts="strict"))
# {'x': {'a': [('1', 1)]}}
```
In this ^ case, I'd want to get the value: `{'x': {'a': {'1': 1}}}`, such that round trips would work as expected.
### What changes are included in this PR?
Begin to apply the `maps_as_pydicts` to nested values in map types as well, update relevant test.
### Are these changes tested?
Yes
### Are there any user-facing changes?
Yes, just a user-facing fix.
* GitHub Issue: apache#47380
Lead-authored-by: Johanna <johanna.goergen@gmail.com>
Co-authored-by: zzkv <johanna.goergen@gmail.com>
Co-authored-by: Johanna <johanna.goergen@optimizely.com>
Signed-off-by: Raúl Cumplido <raulcumplido@gmail.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

@jo-migo@AlenkaF@jonasdedden@raulcd