blob: 06315a4b398363f5f33d827f6eb312d2c921d79e [file] [log] [blame]
.. Licensed to the Apache Software Foundation (ASF) under one
.. or more contributor license agreements. See the NOTICE file
.. distributed with this work for additional information
.. regarding copyright ownership. The ASF licenses this file
.. to you under the Apache License, Version 2.0 (the
.. "License"); you may not use this file except in compliance
.. with the License. You may obtain a copy of the License at
.. http://www.apache.org/licenses/LICENSE-2.0
.. Unless required by applicable law or agreed to in writing,
.. software distributed under the License is distributed on an
.. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
.. KIND, either express or implied. See the License for the
.. specific language governing permissions and limitations
.. under the License.
.. _arrow-pycapsule-interface:
=============================
The Arrow PyCapsule Interface
=============================
Rationale
=========
The :ref:`C data interface <c-data-interface>`, :ref:`C stream interface <c-stream-interface>`
and :ref:`C device interface <c-device-data-interface>` allow moving Arrow data between
different implementations of Arrow. However, these interfaces don't specify how
Python libraries should expose these structs to other libraries. Prior to this,
many libraries simply provided export to PyArrow data structures, using the
``_import_from_c`` and ``_export_to_c`` methods. However, this always required
PyArrow to be installed. In addition, those APIs could cause memory leaks if
handled improperly.
This interface allows any library to export Arrow data structures to other
libraries that understand the same protocol.
Goals
-----
* Standardize the `PyCapsule`_ objects that represent ``ArrowSchema``, ``ArrowArray``,
``ArrowArrayStream``, ``ArrowDeviceArray`` and ``ArrowDeviceArrayStream``.
* Define standard methods that export Arrow data into such capsule objects,
so that any Python library wanting to accept Arrow data as input can call the
corresponding method instead of hardcoding support for specific Arrow
producers.
Non-goals
---------
* Standardize what public APIs should be used for import. This is left up to
individual libraries.
PyCapsule Standard
==================
When exporting Arrow data through Python, the C Data Interface / C Stream Interface
structures should be wrapped in capsules. Capsules avoid invalid access by
attaching a name to the pointer and avoid memory leaks by attaching a destructor.
Thus, they are much safer than passing pointers as integers.
`PyCapsule`_ allows for a ``name`` to be associated with the capsule, allowing
consumers to verify that the capsule contains the expected kind of data. To make sure
Arrow structures are recognized, the following names must be used:
.. list-table::
:widths: 25 25
:header-rows: 1
* - C Interface Type
- PyCapsule Name
* - ArrowSchema
- ``arrow_schema``
* - ArrowArray
- ``arrow_array``
* - ArrowArrayStream
- ``arrow_array_stream``
* - ArrowDeviceArray
- ``arrow_device_array``
* - ArrowDeviceArrayStream
- ``arrow_device_array_stream``
Lifetime Semantics
------------------
The exported PyCapsules should have a destructor that calls the
:ref:`release callback <c-data-interface-released>`
of the Arrow struct, if it is not already null. This prevents a memory leak in
case the capsule was never passed to another consumer.
If the capsule has been passed to a consumer, the consumer should have moved
the data and marked the release callback as null, so there isn’t a risk of
releasing data the consumer is using.
:ref:`Read more in the C Data Interface specification <c-data-interface-released>`.
In case of a device struct, the above mentioned release callback is the
``release`` member of the embedded ``ArrowArray`` structure.
:ref:`Read more in the C Device Interface specification <c-device-data-interface-semantics>`.
Just like in the C Data Interface, the PyCapsule objects defined here can only
be consumed once.
For an example of a PyCapsule with a destructor, see `Create a PyCapsule`_.
Export Protocol
===============
The interface consists of three separate protocols:
* ``ArrowSchemaExportable``, which defines the ``__arrow_c_schema__`` method.
* ``ArrowArrayExportable``, which defines the ``__arrow_c_array__`` method.
* ``ArrowStreamExportable``, which defines the ``__arrow_c_stream__`` method.
Two additional protocols are defined for the Device interface:
* ``ArrowDeviceArrayExportable``, which defines the ``__arrow_c_device_array__`` method.
* ``ArrowDeviceStreamExportable``, which defines the ``__arrow_c_device_stream__`` method.
ArrowSchema Export
------------------
Schemas, fields, and data types can implement the method ``__arrow_c_schema__``.
.. py:method:: __arrow_c_schema__(self)
Export the object as an ArrowSchema.
:return: A PyCapsule containing a C ArrowSchema representation of the
object. The capsule must have a name of ``"arrow_schema"``.
ArrowArray Export
-----------------
Arrays and record batches (contiguous tables) can implement the method
``__arrow_c_array__``.
.. py:method:: __arrow_c_array__(self, requested_schema=None)
Export the object as a pair of ArrowSchema and ArrowArray structures.
:param requested_schema: A PyCapsule containing a C ArrowSchema representation
of a requested schema. Conversion to this schema is best-effort. See
`Schema Requests`_.
:type requested_schema: PyCapsule or None
:return: A pair of PyCapsules containing a C ArrowSchema and ArrowArray,
respectively. The schema capsule should have the name ``"arrow_schema"``
and the array capsule should have the name ``"arrow_array"``.
Libraries supporting the Device interface can implement a ``__arrow_c_device_array__``
method on those objects, which works the same as ``__arrow_c_array__`` except
for returning an ArrowDeviceArray structure instead of an ArrowArray structure:
.. py:method:: __arrow_c_device_array__(self, requested_schema=None, **kwargs)
Export the object as a pair of ArrowSchema and ArrowDeviceArray structures.
:param requested_schema: A PyCapsule containing a C ArrowSchema representation
of a requested schema. Conversion to this schema is best-effort. See
`Schema Requests`_.
:type requested_schema: PyCapsule or None
:param kwargs: Additional keyword arguments should only be accepted if they have
a default value of ``None``, to allow for future addition of new keywords.
See :ref:`arrow-pycapsule-interface-device-support` for more details.
:return: A pair of PyCapsules containing a C ArrowSchema and ArrowDeviceArray,
respectively. The schema capsule should have the name ``"arrow_schema"``
and the array capsule should have the name ``"arrow_device_array"``.
ArrowStream Export
------------------
Tables / DataFrames and streams can implement the method ``__arrow_c_stream__``.
.. py:method:: __arrow_c_stream__(self, requested_schema=None)
Export the object as an ArrowArrayStream.
:param requested_schema: A PyCapsule containing a C ArrowSchema representation
of a requested schema. Conversion to this schema is best-effort. See
`Schema Requests`_.
:type requested_schema: PyCapsule or None
:return: A PyCapsule containing a C ArrowArrayStream representation of the
object. The capsule must have a name of ``"arrow_array_stream"``.
Libraries supporting the Device interface can implement a ``__arrow_c_device_stream__``
method on those objects, which works the same as ``__arrow_c_stream__`` except
for returning an ArrowDeviceArrayStream structure instead of an ArrowArrayStream
structure:
.. py:method:: __arrow_c_device_stream__(self, requested_schema=None, **kwargs)
Export the object as an ArrowDeviceArrayStream.
:param requested_schema: A PyCapsule containing a C ArrowSchema representation
of a requested schema. Conversion to this schema is best-effort. See
`Schema Requests`_.
:type requested_schema: PyCapsule or None
:param kwargs: Additional keyword arguments should only be accepted if they have
a default value of ``None``, to allow for future addition of new keywords.
See :ref:`arrow-pycapsule-interface-device-support` for more details.
:return: A PyCapsule containing a C ArrowDeviceArrayStream representation of the
object. The capsule must have a name of ``"arrow_device_array_stream"``.
Schema Requests
---------------
In some cases, there might be multiple possible Arrow representations of the
same data. For example, a library might have a single integer type, but Arrow
has multiple integer types with different sizes and sign. As another example,
Arrow has several possible encodings for an array of strings: 32-bit offsets,
64-bit offsets, string view, and dictionary-encoded. A sequence of strings could
export to any one of these Arrow representations.
In order to allow the caller to request a specific representation, the
:meth:`__arrow_c_array__` and :meth:`__arrow_c_stream__` methods take an optional
``requested_schema`` parameter. This parameter is a PyCapsule containing an
``ArrowSchema``.
The callee should attempt to provide the data in the requested schema. However,
if the callee cannot provide the data in the requested schema, they may return
with the same schema as if ``None`` were passed to ``requested_schema``.
If the caller requests a schema that is not compatible with the data,
say requesting a schema with a different number of fields, the callee should
raise an exception. The requested schema mechanism is only meant to negotiate
between different representations of the same data and not to allow arbitrary
schema transformations.
.. _PyCapsule: https://docs.python.org/3/c-api/capsule.html
.. _arrow-pycapsule-interface-device-support:
Device Support
--------------
The PyCapsule interface has cross hardware support through using the
:ref:`C device interface <c-device-data-interface>`. This means it is possible
to exchange data on non-CPU devices (e.g. CUDA GPUs) and to inspect on what
device the exchanged data lives.
For exchanging the data structures, this interface has two sets of protocol
methods: the standard CPU-only versions (:meth:`__arrow_c_array__` and
:meth:`__arrow_c_stream__`) and the equivalent device-aware versions
(:meth:`__arrow_c_device_array__`, and :meth:`__arrow_c_device_stream__`).
For CPU-only producers, it is allowed to either implement only the standard
CPU-only protocol methods, or either implement both the CPU-only and device-aware
methods. The absence of the device version methods implies CPU-only data. For
CPU-only consumers, it is encouraged to be able to consume both versions of the
protocol.
For a device-aware producer whose data structures can only reside in
non-CPU memory, it is recommended to only implement the device version of the
protocol (e.g. only add ``__arrow_c_device_array__``, and not add ``__arrow_c_array__``).
Producers that have data structures that can live both on CPU or non-CPU devices
can implement both versions of the protocol, but the CPU-only versions
(:meth:`__arrow_c_array__` and :meth:`__arrow_c_stream__`) should be guaranteed
to contain valid pointers for CPU memory (thus, when trying to export non-CPU data,
either raise an error or make a copy to CPU memory).
Producing the ``ArrowDeviceArray`` and ``ArrowDeviceArrayStream`` structures
is expected to not involve any cross-device copying of data.
The device-aware methods (:meth:`__arrow_c_device_array__`, and :meth:`__arrow_c_device_stream__`)
should accept additional keyword arguments (``**kwargs``), if they have a
default value of ``None``. This allows for future addition of new optional
keywords, where the default value for such a new keyword will always be ``None``.
The implementor is responsible for raising a ``NotImplementedError`` for any
additional keyword being passed by the user which is not recognised. For
example:
.. code-block:: python
def __arrow_c_device_array__(self, requested_schema=None, **kwargs):
non_default_kwargs = [
name for name, value in kwargs.items() if value is not None
]
if non_default_kwargs:
raise NotImplementedError(
f"Received unsupported keyword argument(s): {non_default_kwargs}"
)
...
Protocol Typehints
------------------
The following typehints can be copied into your library to annotate that a
function accepts an object implementing one of these protocols.
.. code-block:: python
from typing import Tuple, Protocol
class ArrowSchemaExportable(Protocol):
def __arrow_c_schema__(self) -> object: ...
class ArrowArrayExportable(Protocol):
def __arrow_c_array__(
self,
requested_schema: object | None = None
) -> Tuple[object, object]:
...
class ArrowStreamExportable(Protocol):
def __arrow_c_stream__(
self,
requested_schema: object | None = None
) -> object:
...
class ArrowDeviceArrayExportable(Protocol):
def __arrow_c_device_array__(
self,
requested_schema: object | None = None,
**kwargs,
) -> Tuple[object, object]:
...
class ArrowDeviceStreamExportable(Protocol):
def __arrow_c_device_stream__(
self,
requested_schema: object | None = None,
**kwargs,
) -> object:
...
Examples
========
Create a PyCapsule
------------------
To create a PyCapsule, use the `PyCapsule_New <https://docs.python.org/3/c-api/capsule.html#c.PyCapsule_New>`_
function. The function must be passed a destructor function that will be called
to release the data the capsule points to. It must first call the release
callback if it is not null, then free the struct.
Below is the code to create a PyCapsule for an ``ArrowSchema``. The code for
``ArrowArray`` and ``ArrowArrayStream`` is similar.
.. tab-set::
.. tab-item:: C
.. code-block:: c
#include <Python.h>
void ReleaseArrowSchemaPyCapsule(PyObject* capsule) {
struct ArrowSchema* schema =
(struct ArrowSchema*)PyCapsule_GetPointer(capsule, "arrow_schema");
if (schema->release != NULL) {
schema->release(schema);
}
free(schema);
}
PyObject* ExportArrowSchemaPyCapsule() {
struct ArrowSchema* schema =
(struct ArrowSchema*)malloc(sizeof(struct ArrowSchema));
// Fill in ArrowSchema fields
// ...
return PyCapsule_New(schema, "arrow_schema", ReleaseArrowSchemaPyCapsule);
}
.. tab-item:: Cython
.. code-block:: cython
cimport cpython
from libc.stdlib cimport malloc, free
cdef void release_arrow_schema_py_capsule(object schema_capsule):
cdef ArrowSchema* schema = <ArrowSchema*>cpython.PyCapsule_GetPointer(
schema_capsule, 'arrow_schema'
)
if schema.release != NULL:
schema.release(schema)
free(schema)
cdef object export_arrow_schema_py_capsule():
cdef ArrowSchema* schema = <ArrowSchema*>malloc(sizeof(ArrowSchema))
# It's recommended to immediately wrap the struct in a capsule, so
# if subsequent lines raise an exception memory will not be leaked.
schema.release = NULL
capsule = cpython.PyCapsule_New(
<void*>schema, 'arrow_schema', release_arrow_schema_py_capsule
)
# Fill in ArrowSchema fields:
# schema.format = ...
# ...
return capsule
Consume a PyCapsule
-------------------
To consume a PyCapsule, use the `PyCapsule_GetPointer <https://docs.python.org/3/c-api/capsule.html#c.PyCapsule_GetPointer>`_ function
to get the pointer to the underlying struct. Import the struct using your
system's Arrow C Data Interface import function. Only after that should the
capsule be freed.
The below example shows how to consume a PyCapsule for an ``ArrowSchema``. The
code for ``ArrowArray`` and ``ArrowArrayStream`` is similar.
.. tab-set::
.. tab-item:: C
.. code-block:: c
#include <Python.h>
// If the capsule is not an ArrowSchema, will return NULL and set an exception.
struct ArrowSchema* GetArrowSchemaPyCapsule(PyObject* capsule) {
return PyCapsule_GetPointer(capsule, "arrow_schema");
}
.. tab-item:: Cython
.. code-block:: cython
cimport cpython
cdef ArrowSchema* get_arrow_schema_py_capsule(object capsule) except NULL:
return <ArrowSchema*>cpython.PyCapsule_GetPointer(capsule, 'arrow_schema')
Backwards Compatibility with PyArrow
------------------------------------
When interacting with PyArrow, the PyCapsule interface should be preferred over
the ``_export_to_c`` and ``_import_from_c`` methods. However, many libraries will
want to support a range of PyArrow versions. This can be done via Duck typing.
For example, if your library had an import method such as:
.. code-block:: python
# OLD METHOD
def from_arrow(arr: pa.Array)
array_import_ptr = make_array_import_ptr()
schema_import_ptr = make_schema_import_ptr()
arr._export_to_c(array_import_ptr, schema_import_ptr)
return import_c_data(array_import_ptr, schema_import_ptr)
You can rewrite this method to support both PyArrow and other libraries that
implement the PyCapsule interface:
.. code-block:: python
# NEW METHOD
def from_arrow(arr)
# Newer versions of PyArrow as well as other libraries with Arrow data
# implement this method, so prefer it over _export_to_c.
if hasattr(arr, "__arrow_c_array__"):
schema_ptr, array_ptr = arr.__arrow_c_array__()
return import_c_capsule_data(schema_ptr, array_ptr)
elif isinstance(arr, pa.Array):
# Deprecated method, used for older versions of PyArrow
array_import_ptr = make_array_import_ptr()
schema_import_ptr = make_schema_import_ptr()
arr._export_to_c(array_import_ptr, schema_import_ptr)
return import_c_data(array_import_ptr, schema_import_ptr)
else:
raise TypeError(f"Cannot import {type(arr)} as Arrow array data.")
You may also wish to accept objects implementing the protocol in your
constructors. For example, in PyArrow, the :func:`array` and :func:`record_batch`
constructors accept any object that implements the :meth:`__arrow_c_array__` method
protocol. Similarly, the PyArrow's :func:`schema` constructor accepts any object
that implements the :meth:`__arrow_c_schema__` method.
Now if your library has an export to PyArrow function, such as:
.. code-block:: python
# OLD METHOD
def to_arrow(self) -> pa.Array:
array_export_ptr = make_array_export_ptr()
schema_export_ptr = make_schema_export_ptr()
self.export_c_data(array_export_ptr, schema_export_ptr)
return pa.Array._import_from_c(array_export_ptr, schema_export_ptr)
You can rewrite this function to use the PyCapsule interface by passing your
object to the :py:func:`array` constructor, which accepts any object that
implements the protocol. An easy way to check if the PyArrow version is new
enough to support this is to check whether ``pa.Array`` has the
``__arrow_c_array__`` method.
.. code-block:: python
import warnings
# NEW METHOD
def to_arrow(self) -> pa.Array:
# PyArrow added support for constructing arrays from objects implementing
# __arrow_c_array__ in the same version it added the method for it's own
# arrays. So we can use hasattr to check if the method is available as
# a proxy for checking the PyArrow version.
if hasattr(pa.Array, "__arrow_c_array__"):
return pa.array(self)
else:
array_export_ptr = make_array_export_ptr()
schema_export_ptr = make_schema_export_ptr()
self.export_c_data(array_export_ptr, schema_export_ptr)
return pa.Array._import_from_c(array_export_ptr, schema_export_ptr)
Comparison with Other Protocols
===============================
Comparison to DataFrame Interchange Protocol
--------------------------------------------
`The DataFrame Interchange Protocol <https://data-apis.org/dataframe-protocol/latest/>`_
is another protocol in Python that allows for the sharing of data between libraries.
This protocol is complementary to the DataFrame Interchange Protocol. Many of
the objects that implement this protocol will also implement the DataFrame
Interchange Protocol.
This protocol is specific to Arrow-based data structures, while the DataFrame
Interchange Protocol allows non-Arrow data frames and arrays to be shared as well.
Because of this, these PyCapsules can support Arrow-specific features such as
nested columns.
This protocol is also much more minimal than the DataFrame Interchange Protocol.
It just handles data export, rather than defining accessors for details like
number of rows or columns.
In summary, if you are implementing this protocol, you should also consider
implementing the DataFrame Interchange Protocol.
Comparison to ``__arrow_array__`` protocol
------------------------------------------
The :ref:`arrow_array_protocol` protocol is a dunder method that
defines how PyArrow should import an object as an Arrow array. Unlike this
protocol, it is specific to PyArrow and isn't used by other libraries. It is
also limited to arrays and does not support schemas, tabular structures, or streams.