Skip to content

GH-55263: [Python][Annotations] Add internal types and core data structures - #55264

Draft
rok wants to merge 50 commits into
apache:mainfrom
rok:pyarrow-stubs-pr3-core-data-sans-generic
Draft

rok wants to merge 50 commits into
apache:mainfrom
rok:pyarrow-stubs-pr3-core-data-sans-generic

Conversation

@rok

@rok rok commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Rationale for this change

This is the next step in adding type annotations to PyArrow (#32609). It merges #49103 (internal type system stubs) and #49193 (core data structures) because the two depend on each other and can't be type checked separately.

What changes are included in this PR?

Are these changes tested?

Are there any user-facing changes?

Was AI used for this PR?

In accordance to the AI generation guidelines, please disclose below whether and how AI was used in this PR.

PR code and description written by:

  • Human
  • AI

Reviewed before submission by:

  • Human
  • AI
  • Not reviewed

rok and others added 25 commits October 26, 2026 11:30
Co-authored-by: Dan Redding <125183946+dangotbanned@users.noreply.github.com>
Co-authored-by: Dan Redding <125183946+dangotbanned@users.noreply.github.com>
Co-authored-by: Dan Redding <125183946+dangotbanned@users.noreply.github.com>
Co-authored-by: Dan Redding <125183946+dangotbanned@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Add type annotation stubs for array, table, tensor, builder, memory,
device, config, and types modules. Includes type-ignore annotations in related tests.
Drop the TypeVar/Generic parameters from the DataType subclasses,
Field, Array, ChunkedArray and the tabular classes. Where a property
can only return one of a few types (dictionary index type, run end
type, time units) it now returns a union or Literal instead.

Most of these parameters weren't buying much yet: scalar.pyi and
lib.pyi are still placeholders, so e.g. Array[Scalar[...]] collapsed
to Any anyway, and several annotations were being flagged by mypy and
pyright (TypeVars used only once, type arguments on subclasses that
weren't actually generic). Plain classes are a lot easier to read and
review, and we can bring parameters back later where they clearly
pay off.

This also removes the stub-only _BasicDataType and _HasPrecisionScale
helpers, which have no runtime counterpart, plus the IntoArray and
SingleOrList aliases.
Add pyarrow-stubs/tests with pandas-stubs style checks:

    check(assert_type(lib.list_(lib.int32()), lib.ListType), lib.ListType)

assert_type is verified by mypy, pyright and ty, which already include
pyarrow-stubs/ in their runs, and check() verifies the runtime class when
the tests run under pytest, so the stubs can't silently drift from what
pyarrow returns. Usage that should be rejected goes under
`if TYPE_CHECKING_INVALID_USAGE:` with a `# type: ignore[...]`; unused
ignores are errors in these tests, so those fail if the usage starts
type checking.

To have something to test, lib.pyi now re-exports the stub modules that
are annotated so far, still falling back to Any for everything else.

The CI type annotation script runs the tests with pytest after the type
checkers.
@github-actions github-actions Bot added the awaiting committer review Awaiting committer review label Oct 26, 2026
@github-actions

Copy link
Copy Markdown

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

table.pyi imports RecordBatchReader from ._ipc, which had no stub, so
pyright and ty failed to resolve the import. Add a placeholder in the
same style as io.pyi and scalar.pyi until _ipc gets fully annotated.
pip install fsspec pandas-stubs scipy-stubs types-cffi types-psutil types-requests types-python-dateutil

# Install type checkers
pip install mypy pyright ty

@tadeja tadeja Nov 2, 2026 •

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.

scratch note: should we add "typing_extensions>=4.12.0" also here to be certain to have it? Although CI job log of Conda Python 3.12 Sphinx & Numpydoc already shows the package in the conda env where pyarrow-stubs/tests currently run OK; typing_extensions 4.16.0 pyhcf101f3_0 conda-forge

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 mypy (at least) installs typing_extensions as transitive dependency. Not sure if we need to install typing_extensions separately (yet).

@github-actions github-actions Bot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Nov 2, 2026
@tadeja

tadeja commented Nov 3, 2026

Copy link
Copy Markdown
Member

A note while I'm still reviewing array.pyi. With 3ddf085:
__lib/array.pyi - removed duplicating NullableCollection, added optional import ignores (like in types.pyi already), and corrected __dlpack__ signature
tests/test_array.py a new regression test for DLPack
( Optional import ignores not yet added for _stubs_typing.pyi, __lib/tensor.pyi or __lib/table.pyi )

Type checking without NumPy, pandas, SciPy

uv run --isolated --no-project --with mypy==2.4.0 --with pyright==1.1.414 --with ty==0.0.92 bash -c 'mypy --no-incremental; pyright; ty check'

Resolved:
pyarrow-stubs/pyarrow/__lib/array.pyi:29: error: Cannot find implementation or library stub for module named "numpy" [import-not-found]
pyarrow-stubs/pyarrow/__lib/array.pyi:29: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
pyarrow-stubs/pyarrow/__lib/array.pyi:30: error: Library stubs not installed for "pandas" [import-untyped]
pyarrow-stubs/pyarrow/__lib/array.pyi:30: note: Hint: "python3 -m pip install pandas-stubs"
pyarrow-stubs/pyarrow/__lib/array.pyi:30: note: (or run "mypy --install-types" to install all missing stub packages)

Remaining:
pyarrow-stubs/pyarrow/_stubs_typing.pyi:30: error: Cannot find implementation or library stub for module named "numpy" [import-not-found]
pyarrow-stubs/pyarrow/_stubs_typing.pyi:32: error: Cannot find implementation or library stub for module named "numpy.typing" [import-not-found]
pyarrow-stubs/pyarrow/__lib/tensor.pyi:26: error: Cannot find implementation or library stub for module named "numpy" [import-not-found]
pyarrow-stubs/pyarrow/__lib/tensor.pyi:30: error: Library stubs not installed for "scipy.sparse" [import-untyped]
pyarrow-stubs/pyarrow/__lib/tensor.pyi:30: note: Hint: "python3 -m pip install scipy-stubs"
pyarrow-stubs/pyarrow/__lib/table.pyi:32: error: Cannot find implementation or library stub for module named "numpy" [import-not-found]
pyarrow-stubs/pyarrow/__lib/table.pyi:33: error: Library stubs not installed for "pandas" [import-untyped]
pyarrow-stubs/pyarrow/__lib/table.pyi:33: note: Hint: "python3 -m pip install pandas-stubs"
pyarrow-stubs/pyarrow/__lib/table.pyi:33: note: (or run "mypy --install-types" to install all missing stub packages)
pyarrow-stubs/pyarrow/__lib/table.pyi:35: error: Cannot find implementation or library stub for module named "numpy.typing" [import-not-found]
pyarrow-stubs/pyarrow/__lib/table.pyi:35: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
Found 7 errors in 3 files (checked 22 source files)
/Users/tadeja/Documents/arrow/python/pyarrow-stubs/pyarrow/__lib/table.pyi
/Users/tadeja/Documents/arrow/python/pyarrow-stubs/pyarrow/__lib/table.pyi:32:8 - error: Import "numpy" could not be resolved (reportMissingImports)
/Users/tadeja/Documents/arrow/python/pyarrow-stubs/pyarrow/__lib/table.pyi:33:8 - error: Import "pandas" could not be resolved (reportMissingImports)
/Users/tadeja/Documents/arrow/python/pyarrow-stubs/pyarrow/__lib/table.pyi:35:6 - error: Import "numpy.typing" could not be resolved (reportMissingImports)
/Users/tadeja/Documents/arrow/python/pyarrow-stubs/pyarrow/__lib/tensor.pyi
/Users/tadeja/Documents/arrow/python/pyarrow-stubs/pyarrow/__lib/tensor.pyi:26:8 - error: Import "numpy" could not be resolved (reportMissingImports)
/Users/tadeja/Documents/arrow/python/pyarrow-stubs/pyarrow/__lib/tensor.pyi:30:6 - error: Import "scipy.sparse" could not be resolved (reportMissingImports)
/Users/tadeja/Documents/arrow/python/pyarrow-stubs/pyarrow/_stubs_typing.pyi
/Users/tadeja/Documents/arrow/python/pyarrow-stubs/pyarrow/_stubs_typing.pyi:30:8 - error: Import "numpy" could not be resolved (reportMissingImports)
/Users/tadeja/Documents/arrow/python/pyarrow-stubs/pyarrow/_stubs_typing.pyi:32:6 - error: Import "numpy.typing" could not be resolved (reportMissingImports)
7 errors, 0 warnings, 0 informations
All checks passed!

@AlenkaF

AlenkaF commented Nov 3, 2026

Copy link
Copy Markdown
Member

I am reviewing __lib/table.pyi at the moment and will add the import ignores there too 👍

@tadeja
tadeja force-pushed the pyarrow-stubs-pr3-core-data-sans-generic branch from 8e8a811 to aac6c4b Compare November 3, 2026 18:46
@github-actions github-actions Bot added awaiting changes Awaiting changes and removed awaiting change review Awaiting change review labels Nov 3, 2026
@github-actions github-actions Bot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Nov 5, 2026
@AlenkaF

AlenkaF commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

@tadeja @rok I have checked table.pyi and pushed the changes in commit (d9f0adb). Posting reasons for most of the changes here:

ChunkedArray

RecordBatch

  • Updated mapping in from_pydict to be in line with values in asarray method (array.pyi)
  • I suggest removing the use of Generator[*, None, None] and use Iterator[*] instead
  • Cython defines list arrays in from_arrays() so I corrected the type from Iterable[Any] to list[Any]. Similar for struct_array in from_struct_array
  • In from_pandas the reason for the change from Sequence to Iterable is because of the use of columns in _resolve_columns_of_interest.
  • In equals the parameter other is changed to object as in the Cython signature. If other is anything else than RecordBatch at runtime, the method returns False
  • Removing table_to_blocks as it is an internal helper. Similar to string_to_tzinfo, tzinfo_to_string and ensure_metadata which are not in the stubs currently. Later we should add them to the stubtest allowlist.

Table

  • from_arrays column parameter should be Sequence instead of Collections as types without __getitem__ error at runtime, added an example. Also changed list to Iterable so that tuple also works (which should also cover NDArray[Any])
  • column in add_column takes ChunkedArray or anything array-like that chunked_array supports so I created an alias for the array-like chunked version (ChunkedArrayConstructorInput) and used that in add_column and set_column.
  • Found a bug in record_batch in the data parameter. Tuple is erroring at runtime (pa.record_batch(([1, 2],), names=["a"]) gives TypeError: Argument 'arrays' has incorrect type (expected list, got tuple)). I created a separate issue for this: [Python] pa.record_batch raises TypeError for tuple data #55418. In the stubs I changed the Collection to list[Any] so it is equal to RecordBatch.from_array() as that is what is used at runtime. I also changed Mapping to dict as it is also checked directly via isinstance(data, dict) in the code.

Note: I am not sure about Mappings in from_pydict and from_pylist, will have a separate look at this tomorrow.

@tadeja I also created MapsAsPydicts type alias and added a use of that in array.pyi. Hope that is OK.

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

Great work!

I did a (partial) pass though, and added a bunch of comments (sorry for the overload!). Some themes emerging from the inline comments, partially repeating here:

  • How should the type checkers be run? Through pre-commit, through the python_test_type_annotations.sh script? Manually calling mypy or one of the others (in your typical local dev env)?
    An important aspect here is that (in my experience) we have to control the exact set of versions of all dependencies (type checkers + pyarrow (test) deps). This is going to get messy if we have three ways to run them.

    I suppose listing those manually in pre-commit will have to stay (that cannot reference an external file), but so at least let's add a comment there that this has to be kept in sync with the list of deps used in CI. And for the CI script, we could maybe have a dependency-group in pyproject.toml specifying those? (that makes it more easily reused)

  • Do we already want to run the type checkers on the pyarrow tests? (or a subset of the test files) The PR seems to not yet do this, but on the other hand has a bunch of (partly incorrect?) changes to the test files (I started adding comments on those changes, but then stopped doing that). I would make a choice one way or the other.
    (if we are currently not actually running the type checkers against the tests code, I would leave out all changes to those files for a subsequent PR; the changes are not lost, they can be cherry-picked in a follow-up PR)

  • But then related to actually running the type checkers on the tests:

    • Should we specify --follow-untyped-imports for mypy? That avoids errors for me when I run it on eg pyarrow/tests/test_array.py complaining about pandas (and others) not being typed (because pandas-stubs is not installed, but the main pandas package also has annotations that mypy can find).
      It would also avoid that any annotation using a pandas object gets interpreted as Any (also for the stub tests).

    • The type checkers actually hardly report any error for me locally, but I think that is because the stubs don't actually exposes the public API in pyarrow-stubs/__init__.pyi (this might be an easy from pyarrow.lib import * to be added?).

  • This does not seem to work out of the box with a local development (editable) install, to have other tools (eg IDE) pick up the stubs in that case.
    I tried with opening a new vscode window, select my dev env as python interpreter, and start a python file with some pyarrow code. It could infer types when hovering or doing tab completion (and this does work when selecting a python env that has pyarrow-stubs installed)

  • Should we add stubtest to CI? (that seems important to ensure the code and stubs don't drift on subsequent changes)

  • It would be good to get some consistency in how certain input arguments are typed, eg for array-like input, I see all of Iterable, Sequence and list, while I think those all three should be the same (often, we might also functions where we are in practice stricter)

  • I left a few inline comments related to this (but there are many other places doing it as well), but we should discuss how to type input arguments requiring a type specific Array (like BooleanArray for a mask, or IntegerArray for indices). I think we should type those all as just Array, as otherwise users will get annoying type errors if their variables are inferred as just Array (which will often be the case)

ty check --verbose --output-format concise

# The type annotation tests are type checked above; also run them to verify
# the annotated types against the installed pyarrow.

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.

Suggested change
# the annotated types against the installed pyarrow.
# the expected annotated types against the runtime types.

to make it clear that this pytest invocation does not actually check annotations (it only does because we repeat it twice, but so in itself just checks the runtime isinstance).

(I was confused about what this pytest would do, in addition to running the type checkers)

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.

Would it also help to mention the description of assert_type and check methods from the python/pyarrow-stubs/tests/_check.py docstring?

Comment on lines +214 to +216
Run ``pytest`` directly and pass it the directory: ``python -m pytest`` puts
``arrow/python`` on ``sys.path``, so the source tree would be imported instead
of the installed PyArrow.

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 don't know if this is hallucinated or because someone of you actually run into an issue with this, but using python -m pytest instead of pytest with the example above works just fine for me.

(in general this is just about running pytest, so that should generally work the same as running the normal pyarrow unit tests? And there we don't say anything like this: https://arrow.apache.org/docs/developers/python/development.html#unit-testing)

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.

python -m pytest pyarrow-stubs/tests from arrow/python works for me too (the tests pass).

Comment thread python/pyarrow-stubs/tests/_check.py Resolved
def test_array() -> None:
# pa.array() isn't annotated with a specific return type yet, so tell
# the type checker which class to expect.
arr = check(cast(lib.Int64Array, lib.array([1, 2, None])), lib.Int64Array)

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.

Is there a reason this is all written with lib instead of the public import path? (to be honest, if using pa. for that, this is only about a single character being saved ;), but in general in tests we want to use public imports; that also makes things easier to copy or paste from interactive testing)

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 reason is the placeholder in the __init__.pyi. Until that file is also updated (following PRs) all pa.* will be resolved to Any (due to __getattr__).

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.

AFAIK there is no reason to not already expose everything currently typed in __init__.pyi, as that is compatible with keeping the __getattr__ as a catch-all for everything else. At least in normal python code, a module __getattr__ is only used if a name is not available in the module through normal means.


def test_import_from_c_capsule() -> None:
capsules = lib.array([1, 2]).__arrow_c_array__()
imported = lib.BooleanArray._import_from_c_capsule(*capsules)

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.

Is the BooleanArray (which contrasts with the actual type) intentional?

In general I think the usage pattern for calling _import_from_c_capsule it to always do that using the base class pa.Array

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.

Yeah, I wanted to show that irrespective of the array type the method is called on, object with the type from the PyCapsule will be constructed. If it is misleading, I will change to pa.Array.

def from_pandas(
obj: pd.Series | np.ndarray | ArrayLike,
mask: Mask | None = None,
type: DataType | None = None,

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.

Suggested change
type: DataType | None = None,
type: DataType | str | None = None,

def from_buffers(
type: DataType,
length: int,
buffers: Sequence[Buffer | None],

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.

Strictly speaking, this does not need to be a Sequence (which has getitem), but Collection (or even Sized) is enough

(for children below, which has the same requirements, a Collection is used)

buffers: Sequence[Buffer | None],
null_count: int = -1,
offset=0,
children: NullableCollection | None = None,

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.

Suggested change
children: NullableCollection | None = None,
children: Collection | None = None,

?


from types import TracebackType

from typing_extensions import Self

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.

Suggested change
from typing_extensions import Self
from typing import Self


class ChunkedArray(_PandasConvertible):
@property
def data(self) -> Self: ...

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.

Sidenote, we should probably just remove this attribute, it has been deprecated for years

@github-actions github-actions Bot added awaiting changes Awaiting changes and removed awaiting change review Awaiting change review labels Nov 6, 2026
@jorisvandenbossche

Copy link
Copy Markdown
Member
  • NumPy is also an optional dependency (next to pandas) so I added # type: ignore[..] there too.
    Locally mypy and pyright both pass with only # type: ignore[..] so I removed pyright ignores

  • ty doesn't honor mypy's # type: ignore[...] and # ty: ignore[unresolved-import] fails if numpy/pandas are installed, so I disabled ty unresolved-import in pyproject.toml instead (mypy and pyright still catch broken imports).

Alternatively, could we ensure those optional dependencies are installed whenever doing type checks? (which is already the case in practice?) That avoids having to add such ignores?

  • I propose to use typing_extensions.CapsuleType instead of Any in __arrow_c_schema__.
    Claude added a test for this case, I have reviewed and changed it a bit. typing_extensions dependency had to be added.
    Note: we should leave the Protocols in _stubs_typing.pyi at Any. Polars' public pl.DataFrame and pl.Series, for example, annotate __arrow_c_stream__ -> object so narrowing the Protocol would reject them

Would be good to add a test for that (so we don't accidentally change that to Capsule later)

@AlenkaF

AlenkaF commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Yay, @jorisvandenbossche, thanks for the review! <3
I went through the main comments, will check inline ones after lunch =) These are my opinions, Tadeja and Rok will be able to add more here.

  • How should the type checkers be run?

Yeah, I currently run pre-commit and also all 3 type checkers separately in my local environment. The two runs differ due to possibility of missing deps. I agree we should try to make this more clear and unified, maybe using pre-commit only or providing a requirements file for type checking locally in the dev env? (connected to ignores question)

Do we already want to run the type checkers on the pyarrow tests?

I think we would have to run type checking on the tests locally until we have added all the stubs. That is what we have planned after the reviews and before the merge. See: #55264 (comment)

Should we specify --follow-untyped-imports for mypy?

Yes, I already did some testing, see: #55264 (comment)

Should we add stubtest to CI? (that seems important to ensure the code and stubs don't drift on subsequent changes)

Yes, but that would make sense only after all the parts of PyArrow are typed. We did a stubtest check for the part from this PR and already added changes. See #55264 (comment)

It would be good to get some consistency in how certain input arguments are typed, eg for array-like input, I see all of Iterable, Sequence and list, while I think those all three should be the same (often, we might also functions where we are in practice stricter)

Yes, I think consistency would be good. But going through the code I see we have very diverse way of checking the types in PyArrow. For example, sometimes a parameter is typed in Cython which checks types at runtime so we have to be stricter there. Or we check specifically for list or tuple with isinstance which then also needs a narrower stub. I tried to create and use type aliases more (for example added ChunkedArrayConstructorInput type alias for input when using chunked_array method) but do not yet see how to make this more general.

Alternatively, could we ensure those optional dependencies are installed whenever doing type checks? (which is already the case in practice?) That avoids having to add such ignores?

Yeah, I think that makes sense. If we agree on the first item (how to run stubs) I can push the change and also add some documentation already (or at least add it on my todo).

Would be good to add a test for that (so we don't accidentally change that to Capsule later)

Yes! I have it on my todo list 👍

Co-authored-by: Joris Van den Bossche <jorisvandenbossche@gmail.com>
@github-actions github-actions Bot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Nov 6, 2026
@github-actions github-actions Bot added awaiting changes Awaiting changes and removed awaiting change review Awaiting change review labels Nov 6, 2026
@github-actions github-actions Bot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Nov 6, 2026
Sign up for free to 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