Skip to content

Conversation

@alippai
Copy link
Contributor

@alippai alippai commented Dec 7, 2025

Rationale for this change

Implement #42018

What changes are included in this PR?

Conversion in numpy->arrow direction with multiple string types

Are these changes tested?

Two basic conversion tests added

Are there any user-facing changes?

Yes, adds support to numpy.StringDType as source

I'm not sure what the AI policy is for apache/arrow, this PR was created using OpenAI Codex.

cc @jorisvandenbossche as he opened the original issue

@github-actions
Copy link

github-actions bot commented Dec 7, 2025

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose

Opening GitHub issues ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename the pull request title in the following format?

GH-${GITHUB_ISSUE_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

@alippai alippai changed the title Fix StringDType helper declaration and initialize UTF8 GH-42018: Add numpy.StringDType support Dec 7, 2025
@github-actions
Copy link

github-actions bot commented Dec 7, 2025

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

@alippai
Copy link
Contributor Author

alippai commented Dec 8, 2025

I have no idea if this is the way to implement numpy compat in arrow. Also I’ll add a better test case for the na_value param.

@pitrou
Copy link
Member

pitrou commented Dec 8, 2025

I'm not sure what the AI policy is for apache/arrow, this PR was created using OpenAI Codex.

Did you review the code to ensure it was/looked correct?

@alippai
Copy link
Contributor Author

alippai commented Dec 8, 2025

@pitrou yes! I don’t work with C++ code professionally, so I lack the knowledge to know if eg the numpy 2.0+ feature usage is actually good or ridiculous.

This was my first attempt and experiment with using AI for OSS development. The issue turned out to be more complex than I initially expected. The PR is still small enough, so I’d appreciate the feedback if it’s good direction and I totally understand if you say this is simply not reviewable and would waste the time of the participants.

@pitrou
Copy link
Member

pitrou commented Dec 8, 2025

I might try to take a look but I will first have to get acquainted with the StringDType.

@rok
Copy link
Member

rok commented Dec 8, 2025

@jorisvandenbossche might have insight.

@WillAyd
Copy link
Contributor

WillAyd commented Dec 8, 2025

cc @ngoldbaum

inline npy_string_allocator* ArrowNpyString_acquire_allocator(
const PyArray_StringDTypeObject* descr) {
using Func = npy_string_allocator* (*)(const PyArray_StringDTypeObject*);
auto func = reinterpret_cast<Func>(PyArray_API[316]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these functions exposed or are they only accessible through the PyArray_API?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a reimplementation of the public api as the numpy api version used in pyarrow was below <2.0 and these were “hidden”. This way the code compiles with both old and new numpy. I didn’t find a better example in pyarrow how to onboard new API. The alternative is dropping the numpy 1.26 support (which might be allowed if pyarrow follows SPEC 0)

Copy link
Contributor

Choose a reason for hiding this comment

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

The idea is unfortunately right, I think, since you can't put this in a different compilation unit with NPY_TARGET_VERSION=NPY_2_0_API_VERSION and you can't switch NPY_TARGET_VERSION within the same compilation unit.

Maybe for these specific functions that was actually unnecessary (and if you/@ngoldbaum likes, we could make them always defined in 2.5, it would just segfault if you ever use it, but we/I missed that it is impossible for there to be something to use it on).
That way, it might only be available if build with NumPy 2.5, but that is probably OK in practice, since that is what official builds will use (except maybe for some old Python version).

That said NPY_ABI_VERSION >= 0x02000000 isn't good here. If anything it should be NPY_FEATURE_VERSION (which you can enforce via NPY_TARGET_VERSION to indicate a minimal version of NumPy you support at runtime).

While think it's correct to hack it in this vain, I would suggest to guard it in a way that uses the custom version only when necessary and makes it obvious how to clean it up in the future.
(Even just copy-paste the C definitions with a #ifndef ...!?)

If NumPy offers the required defines (which it will at least on newer versions or with NPY_TARGET_VERSION=NPY_2_0_API_VERSION it is better to stop using it, there is no guarantee it will remain correct for ever.

Copy link
Contributor Author

@alippai alippai Dec 12, 2025

Choose a reason for hiding this comment

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

Removed most of the guards. As I understand the feature and target versions could be lower, but correct me if I’m wrong, please

@github-actions github-actions bot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels Dec 8, 2025
@ngoldbaum
Copy link

ngoldbaum commented Dec 8, 2025

I can take a look at this but I'll keep in mind an AI wrote it...

Status NumPyConverter::AppendStringDTypeValues(Builder* builder) {
auto* descr = reinterpret_cast<PyArray_StringDTypeObject*>(dtype_);

npy_string_allocator* allocator = ArrowNpyString_acquire_allocator(descr);

Choose a reason for hiding this comment

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

FYI for other reviewers: this locks a mutex internally in NumPy.

@alippai
Copy link
Contributor Author

alippai commented Dec 8, 2025

@ngoldbaum thanks for the review. I’ll address them as soon as I figure out what to do with the numpy versions. Also I appreciate the open minded perspective for the AI, I did my best to only submit something what works and doesn’t have unnecessary code.

@alippai alippai requested a review from ngoldbaum December 12, 2025 05:46
@alippai alippai requested a review from seberg December 12, 2025 05:46
@alippai alippai requested a review from WillAyd December 12, 2025 06:25
@alippai
Copy link
Contributor Author

alippai commented Jan 8, 2026

@jorisvandenbossche @raulcd do you think this can go into the next release or is it not realistic?

@raulcd
Copy link
Member

raulcd commented Jan 8, 2026

do you think this can go into the next release or is it not realistic?

The feature freeze is currently scheduled for Monday 12th. I don't have enough knowledge around this to provide a meaningful review before that as I would have to investigate around stringDType. Maybe if @WillAyd / @pitrou / @ngoldbaum have time before that.

Comment on lines +40 to +42
#ifndef NPY_VSTRING
# define NPY_VSTRING 2056
#endif
Copy link
Member

Choose a reason for hiding this comment

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

Do we actually need this? Can IsStringDType just return false if this constant is not defined (this is presumably when compiling with NumPy < 2)?

}

bool IsStringDType(PyArray_Descr* descr) {
return descr != nullptr && descr->type_num == NPY_VSTRING;
Copy link
Member

Choose a reason for hiding this comment

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

The nullptr check seems superfluous, why would one call this function with a null pointer?

inline npy_string_allocator* ArrowNpyString_acquire_allocator(
const PyArray_StringDTypeObject* descr) {
using Func = npy_string_allocator* (*)(const PyArray_StringDTypeObject*);
return reinterpret_cast<Func>(PyArray_API[316])(descr);
Copy link
Member

Choose a reason for hiding this comment

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

Why not call NpyString_acquire_allocator directly? AFAICT it is a macro that does roughly what your code is doing:

#if NPY_FEATURE_VERSION >= NPY_2_0_API_VERSION
#define NpyString_acquire_allocator \
        (*(npy_string_allocator * (*)(const PyArray_StringDTypeObject *)) \
    PyArray_API[316])
#endif


namespace {

#if NPY_ABI_VERSION >= 0x02000000
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure why we're guarding with NPY_ABI_VERSION rather than either NPY_VERSION or NPY_FEATURE_VERSION. Can a NumPy maintainer explain how these 3 macros differ? @ngoldbaum @seberg

(also, would be nice if the NumPy docs were a bit more talkative about this)

Copy link
Contributor

Choose a reason for hiding this comment

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

ABI version is the compile time header version of NumPy. NPY_FEATURE_VERSION is the runtime one you are compiling for.
I.e. by default API that is only available on newer versions is disabled so you can't compile to run with 1.x support but use newer API.

In this particular case (effectively using future API) #if NPY_ABI_VERSION >= 0x02000000 may tell you that a definition is already included in the header, in this case I guess PyArray_StringDTypeObject.
There is no reason to hide such a definition, so we don't (the thing to hide is mostly API table entries).

Maybe there should be a section on "how to use future API depending on the NumPy runtime version" (although for things we really expect it, we may want to add it to the npy2_compat.h header ourselves instead).

FWIW, I still think it makes most sense to wholesale copy-paste the NumPy header definitions. Then add some form of guard (and be it #if NPY_FEATURE_VERSION < NUMPY_2_0_VERSION or what it was), so that when it is exposed by the NumPy headers you stop using it.

Copy link
Member

Choose a reason for hiding this comment

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

We don't seem to define NPY_FEATURE_VERSION anywhere, so that does mean the 2.0 macros are not available in the NumPy headers? If we define NPY_FEATURE_VERSION to 2.0, does that mean that the produced extension with not be compiled with NumPy 1.x?

It would be nice if these interactions were made clearer somewhere.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we define NPY_FEATURE_VERSION to 2.0, does that mean that the produced extension with not be compiled with NumPy 1.x?

Yeah, except what you define would be NPY_TARGET_VERSION. Don't ask me why I used a different name, it was probably silly, but it is what we have now.
I think NPY_TARGET_VERSION is described. But this way of using future API conditionally is not (the only place is that npy_2_compat.h header in NumPy itself uses similar conditionals).

Copy link
Member

Choose a reason for hiding this comment

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

(also, can we assume that NPY_FEATURE_VERSION is always <= NPY_ABI_VERSION? and what about NPY_VERSION, is it something different?)

Comment on lines +377 to +378
return Status::NotImplemented(
"NumPy StringDType requires building PyArrow with NumPy >= 2.0");
Copy link
Member

Choose a reason for hiding this comment

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

Is this at all possible to happen? I got the impression that one cannot use NumPy 2 if PyArrow was compiled for NumPy < 2, am I mistaken @ngoldbaum @seberg ?

Copy link
Contributor

Choose a reason for hiding this comment

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

That is correct, the NumPy C-API import will just barf at you and refuse to run.


@pytest.mark.numpy
def test_array_from_numpy_string_dtype():
dtypes_mod = getattr(np, "dtypes", None)
Copy link
Member

Choose a reason for hiding this comment

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

Can use something like dtypes = pytest.importorskip("numpy.dtypes")

Copy link
Member

Choose a reason for hiding this comment

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

Or, even, better, you can use a pytest fixture:

@pytest.fixture
def string_dtype():
    dtypes = pytest.importorskip("numpy.dtypes")
    dtype_class = getattr(dtypes, "StringDType", None)
    if dtype_class is None:
        pytest.skip("NumPy StringDType not available (NumPy > 2 needed)")
    return dtype_class

and then simply:

@pytest.mark.numpy
def test_array_from_numpy_string_dtype(string_dtype):
    arr = np.array(["some", "strings"], dtype=string_dtype())
    # etc.

assert arrow_arr.type == pa.utf8()
assert arrow_arr.to_pylist() == ["some", "strings"]

arrow_arr = pa.array(arr, type=pa.string())
Copy link
Member

Choose a reason for hiding this comment

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

Note that pa.string() and pa.utf8() are the same thing, it's a bit confusing to use both spellings here.

assert arrow_arr.to_pylist() == ["some", "strings"]

arrow_arr = pa.array(arr, type=pa.string())
assert arrow_arr.type == pa.string()
Copy link
Member

Choose a reason for hiding this comment

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

Can you also call arrow_arr.validate(full=True)? (also in other places)


arrow_arr = pa.array(arr, type=pa.string())
assert arrow_arr.type == pa.string()
assert arrow_arr.to_pylist() == ["some", "strings"]
Copy link
Member

Choose a reason for hiding this comment

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

Can also be written assert arrow_arr.to_pylist() == arr.tolist()

mask = np.array([False, False, True], dtype=bool)
arrow_arr = pa.array(arr, mask=mask)
assert arrow_arr.type == pa.utf8()
assert arrow_arr.null_count == 2
Copy link
Member

Choose a reason for hiding this comment

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

Just validate arrow_arr and it will check null_count consistency.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants