Repository: numpy/numpy
Pull request: ENH: Add partition/rpartition ufunc for string dtypes
Closes #25993.

<!--         ----------------------------------------------------------------
                MAKE SURE YOUR PR GETS THE ATTENTION IT DESERVES!
                ----------------------------------------------------------------

*  FORMAT IT RIGHT:
      https://www.numpy.org/devdocs/dev/development_workflow.html#writing-the-commit-message

*  IF IT'S A NEW FEATURE OR API CHANGE, TEST THE WATERS:
      https://www.numpy.org/devdocs/dev/development_workflow.html#get-the-mailing-list-s-opinion

*  HIT ALL THE GUIDELINES:
      https://numpy.org/devdocs/dev/index.html#guidelines

*  WHAT TO DO IF WE HAVEN'T GOTTEN BACK TO YOU:
      https://www.numpy.org/devdocs/dev/development_workflow.html#getting-your-pr-reviewed
-->


Problem statement:
MAINT: What should we do about np.strings.partition?
As of NumPy 1.x, an array returned from `np.char.partition` has one more dimension than the input array, and includes the separator string n times, where n is the number of items in the input array.

```python3
>>> import numpy as np
>>> arr = np.array(['hello,world', 'something,else'])
>>> np.char.partition(arr, ',')
array([['hello', ',', 'world'],
       ['something', ',', 'else']], dtype='<U9')
```

In the context of calling `partition` on arrays, it doesn't make much sense to me to return the separator n times. So, the question is, should we maybe diverge from the CPython behavior and do something less wasteful than that in NumPy? If so, will we make it to rc1? I can work on a PR as soon as we got some consensus on what to return instead, but if we don't make it, let's remove `np.strings.partition` for now and leave it in `np.char`, so that we have freedom over what its API is going to be.

To me, returning the following 3-tuple makes more sense (maybe others have better ideas):
1. bool array that signifies whether the separator was found (1 byte per item vs 4 bytes per item for unicode arrays)
2. string array for all the items before the separator
3. string array for all the items after the separator

cc @ngoldbaum @mhvk

Hints from issue discussion:
That's a tricky one. Certainly, in your example having 9x4 bytes for each comma or empty string seems excessive. I think if we do anything, though, it should follow the order of CPython, i.e., string array of before, string array of separator/empty, string array of after.

For me, what complicates this further is that `np.char.split` returns an object array of list:
```
In [6]: np.char.partition(np.array(['0 1 2', 'a']), ' ')
Out[6]: 
array([['0', ' ', '1 2'],
       ['a', '', '']], dtype='<U3')

In [7]: np.char.split(np.array(['0 1 2', 'a']))
Out[7]: array([list(['0', '1', '2']), list(['a'])], dtype=object)
```
I don't think it is very nice if `.split()` returns something where you have to index the element to get the parts, while for `.partition()` one first has to index the part (to get all elements).

p.s. Since this is not obvious, perhaps for now leaving it just in `np.char` is not a bad idea...
The problem with `split` is the API contract forces us to produce a ragged array.

`partition` is different because you'll always get 3 entries in the trailing dimension.

I agree it would be best if we could figure out a uniform way to handle this. For `split` we could do things like setting entries that come "after" the last split to empty string or force people specify `maxsplit`, but fundamentally the python API doesn't really make sense for an ndarray library. I'm not sure offhand what's best.

pyarrow doesn't have a `str.partition` equivalent, but it looks like they handle this in `pyarrow.compute.split_pattern` by returning a `ListArray` (e.g. a ragged array):

```
In [4]: a
Out[4]:
<pyarrow.lib.StringArray object at 0x104eaa860>
[
  "hello",
  "world"
]

In [5]: pa.compute.split_pattern(a, pattern="l")
Out[5]:
<pyarrow.lib.ListArray object at 0x158561840>
[
  [
    "he",
    "",
    "o"
  ],
  [
    "wor",
    "d"
  ]
]
```

For the specific question posed in the issue, I think returning a boolean array instead of an array containing empty strings or the partition character is probably best, since I suspect that most people will just convert the array to a bool anyway to see where there are valid splits.
I guess one thing to consider is whether there is any specific implementation that would allow one to write a routine that works on arrays, and automatically works on single strings as well. This is partially what makes me like the tuple route but also makes me feel that the middle one, for separator, should be a string array, with separator or empty as entries. It means one can more easily reconstruct things.
For some historical context, the reason the separator was returned in the python implementation is [to use it as a boolean flag](https://marc.info/?l=python-dev&m=112529473001162&w=2). Reconstructing the original string is still trivial since the user knows the separator they passed to `partition` in the first place. And to me, it makes more sense to return a boolean if the intention is to signal that a match was found or not.

That said, keeping consistency with the single-string case is also very worth doing so if the only way to keep that is to return a string array of separators or empty string, that's fine with me.

I also looked a little bit at the `split` question, it looks like at least ONNX defines a [padded split](https://github.com/onnx/onnx/blob/a600e2fe60fcec561a448aabacc43033e5025220/onnx/reference/ops/op_string_split.py#L25) wrapper around `char.split`, so there's at least one piece of prior art where someone went out of their way to not get the object array of lists, despite the memory overhead.
> For split we could do things like setting entries that come "after" the last split to empty string or force people specify maxsplit

I think we should probably leave `split` alone and have it return an array of dtype `object`. Calling `split` on an ndarray makes little sense and, to me, the current behavior is the least confusing to the end user.

> That said, keeping consistency with the single-string case is also very worth doing so if the only way to keep that is to return a string array of separators or empty string, that's fine with me.

I don't understand what exactly consistency means in this context. Would this be consistent?

```python3
>>> import numpy as np
>>> a = np.array(['hello,world'])
>>> np.strings.partition(a, ',')
(array(['hello'], dtype='<U5'), array([ True]), array(['world'], dtype='<U5'))
>>> a = np.str_('hello,world')
>>> np.strings.partition(a, ',')
(np.str_('hello'), np.True_, np.str_('world'))
```

> I also looked a little bit at the split question, it looks like at least ONNX defines a [padded split](https://github.com/onnx/onnx/blob/a600e2fe60fcec561a448aabacc43033e5025220/onnx/reference/ops/op_string_split.py#L25) wrapper around char.split, so there's at least one piece of prior art where someone went out of their way to not get the object array of lists, despite the memory overhead.

This is interesting. Maybe worth adding it as a separate API then? So have both `numpy.strings.split` and `numpy.string.padded_split`?
> Would this be consistent?

I think so, yes. You can pass a single string and an array into the function and you get back the correct result either way.

>  Maybe worth adding it as a separate API then? So have both numpy.strings.split and numpy.string.padded_split?

Yeah, let's do this and not try to force `split` to have an API that makes sense for an array library. 

Maybe we could do the same for the new API we're spitballing for `partition`? Although I can't think of a good name to use. That way we can leave `partition` in `np.strings`, but then when we come up with a new function that makes more sense, add it and add a note to the `partition` docstring that the other function is probably what people want. You could also do it with a keyword argument, but that's likely less discoverable.

My only concern with hiding `partition` and `rpartition` in numpy 2.0 is it means `np.strings` isn't quite a drop-in replacement for `np.char`. But also [it's not exactly a popular function](https://github.com/search?q=%22np.char.partition%22&type=code) so maybe that's fine.
I think in my example I wasn't quite clear, if one uses `np.string.partition` then there is of course no problem, but I was, arguably wrongly, thinking of a function that takes some string input `s` and then doing `s.partition(...)` - and how it would be nice if the result could be interpreted independently of whether `s` was `str` or array-like. But of course, this would only work for `chararray`, so is not directly relevant to a new *function* in `np.strings`.

Anyway, overall I think for both `partition` and `split`, what is returned by `str` is just not quite useful for what needs to be returned by an array routine. In that respect, it would seem fine to delay this a bit, and think, e.g., where there would be a more general routine that allows one to select which part, or how many splits, one wants.
It would be good to get this off the list of blocking items for `2.0.0rc1`. It should require only a diff of a few lines I think, e.g. adding an underscore to the names to ensure they are not exposed publicly yet. Could you open a PR @lysnikolaou?
@rgommers Opened #26033.
Closed by https://github.com/numpy/numpy/pull/26033.

Commit message: Return array with the separators // optimize using find ufunc results

Patch to review:
diff --git a/numpy/_core/code_generators/generate_umath.py b/numpy/_core/code_generators/generate_umath.py
index b72d13d11c6c..f7149a947a4e 100644
--- a/numpy/_core/code_generators/generate_umath.py
+++ b/numpy/_core/code_generators/generate_umath.py
@@ -1300,6 +1300,16 @@ def english_upper(s):
           docstrings.get('numpy._core.umath._zfill'),
           None,
           ),
+'_partition':
+    Ufunc(3, 3, None,
+          docstrings.get('numpy._core.umath._partition'),
+          None,
+          ),
+'_rpartition':
+    Ufunc(3, 3, None,
+          docstrings.get('numpy._core.umath._rpartition'),
+          None,
+          ),
 }
 
 def indent(st, spaces):
diff --git a/numpy/_core/code_generators/ufunc_docstrings.py b/numpy/_core/code_generators/ufunc_docstrings.py
index d214ffbccb55..645a3c356138 100644
--- a/numpy/_core/code_generators/ufunc_docstrings.py
+++ b/numpy/_core/code_generators/ufunc_docstrings.py
@@ -5028,3 +5028,91 @@ def add_newdoc(place, name, doc):
     array(['001', '-01', '+01'], dtype='<U3')
 
     """)
+
+add_newdoc('numpy._core.umath', '_partition',
+    """
+    Partition each element in ``x1`` around ``x2``.
+
+    For each element in ``x1``, split the element at the first
+    occurrence of ``x2``, and return a 3-tuple containing the part
+    before the separator, the separator itself, and the part after
+    the separator. If the separator is not found, the first item of
+    the tuple will contain the whole string, and the second and third
+    ones will be the empty string.
+
+    Parameters
+    ----------
+    x1 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Input array
+    x2 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Separator to split each string element in ``x1``.
+    x3 : array-like, with any integer dtype
+        The indices of the separator
+
+    Returns
+    -------
+    out : 3-tuple:
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part before the separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part after the separator
+
+    See Also
+    --------
+    str.partition
+
+    Examples
+    --------
+    >>> x = np.array(["Numpy is nice!"])
+    >>> np.strings.partition(x, " ")
+    (array(['Numpy'], dtype='<U5'),
+     array([ True]),
+     array(['is nice!'], dtype='<U8'))
+
+    """)
+
+add_newdoc('numpy._core.umath', '_rpartition',
+    """
+    Partition (split) each element around the right-most separator.
+
+    For each element in ``x1``, split the element at the last
+    occurrence of ``x2``, and return a 3-tuple containing the part
+    before the separator, the separator itself, and the part after
+    the separator. If the separator is not found, the third item of
+    the tuple will contain the whole string, and the first and second
+    ones will be the empty string.
+
+    Parameters
+    ----------
+    x1 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Input array
+    x2 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Separator to split each string element in ``x1``.
+    x3 : array-like, with any integer dtype
+        The indices of the separator
+
+    Returns
+    -------
+    out : 3-tuple:
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part before the separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part after the separator
+
+    See Also
+    --------
+    str.rpartition
+
+    Examples
+    --------
+    >>> a = np.array(['aAaAaA', '  aA  ', 'abBABba'])
+    >>> np.strings.rpartition(a, 'A')
+    (array(['aAaAa', '  a', 'abB'], dtype='<U5'),
+     array([ True,  True,  True]),
+     array(['', '  ', 'Bba'], dtype='<U3'))
+
+    """)
diff --git a/numpy/_core/defchararray.py b/numpy/_core/defchararray.py
index 44754a747cec..43451abf990f 100644
--- a/numpy/_core/defchararray.py
+++ b/numpy/_core/defchararray.py
@@ -18,19 +18,19 @@
 import functools
 
 from .._utils import set_module
-from .numerictypes import bytes_, str_, character
+from .numerictypes import bytes_, str_, character, object_
 from .numeric import ndarray, array as narray, asarray as asnarray
 from numpy._core.multiarray import compare_chararrays
 from numpy._core import overrides
 from numpy.strings import *
 from numpy.strings import multiply as strings_multiply
 from numpy._core.strings import (
-    _partition as partition,
-    _rpartition as rpartition,
     _split as split,
     _rsplit as rsplit,
     _splitlines as splitlines,
     _join as join,
+    _to_bytes_or_str_array,
+    _vec_string,
 )
 
 __all__ = [
@@ -303,6 +303,90 @@ def multiply(a, i):
         raise ValueError("Can only multiply by integers")
 
 
+def partition(a, sep):
+    """
+    Partition each element in `a` around `sep`.
+
+    Calls :meth:`str.partition` element-wise.
+
+    For each element in `a`, split the element as the first
+    occurrence of `sep`, and return 3 strings containing the part
+    before the separator, the separator itself, and the part after
+    the separator. If the separator is not found, return 3 strings
+    containing the string itself, followed by two empty strings.
+
+    Parameters
+    ----------
+    a : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Input array
+    sep : {str, unicode}
+        Separator to split each string element in `a`.
+
+    Returns
+    -------
+    out : ndarray
+        Output array of ``StringDType``, ``bytes_`` or ``str_`` dtype,
+        depending on input types. The output array will have an extra
+        dimension with 3 elements per input element.
+
+    Examples
+    --------
+    >>> x = np.array(["Numpy is nice!"])
+    >>> np.char.partition(x, " ")
+    array([['Numpy', ' ', 'is nice!']], dtype='<U8')
+    
+    See Also
+    --------
+    str.partition
+
+    """
+    return _to_bytes_or_str_array(
+        _vec_string(a, object_, 'partition', (sep,)), a)
+
+
+def rpartition(a, sep):
+    """
+    Partition (split) each element around the right-most separator.
+
+    Calls :meth:`str.rpartition` element-wise.
+
+    For each element in `a`, split the element as the last
+    occurrence of `sep`, and return 3 strings containing the part
+    before the separator, the separator itself, and the part after
+    the separator. If the separator is not found, return 3 strings
+    containing the string itself, followed by two empty strings.
+
+    Parameters
+    ----------
+    a : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Input array
+    sep : str or unicode
+        Right-most separator to split each element in array.
+
+    Returns
+    -------
+    out : ndarray
+        Output array of ``StringDType``, ``bytes_`` or ``str_`` dtype,
+        depending on input types. The output array will have an extra
+        dimension with 3 elements per input element.
+
+    See Also
+    --------
+    str.rpartition
+
+    Examples
+    --------
+    >>> a = np.array(['aAaAaA', '  aA  ', 'abBABba'])
+    >>> np.char.rpartition(a, 'A')
+    array([['aAaAa', 'A', ''],
+       ['  a', 'A', '  '],
+       ['abB', 'A', 'Bba']], dtype='<U5')
+
+    """
+    return _to_bytes_or_str_array(
+        _vec_string(a, object_, 'rpartition', (sep,)), a)
+
+
 @set_module("numpy.char")
 class chararray(ndarray):
     """
diff --git a/numpy/_core/src/umath/string_buffer.h b/numpy/_core/src/umath/string_buffer.h
index 70e275f72a90..67fecc34846d 100644
--- a/numpy/_core/src/umath/string_buffer.h
+++ b/numpy/_core/src/umath/string_buffer.h
@@ -1593,4 +1593,43 @@ string_zfill(Buffer<enc> buf, npy_int64 width, Buffer<enc> out)
 }
 
 
+template <ENCODING enc>
+static inline void
+string_partition(Buffer<enc> buf1, Buffer<enc> buf2, npy_int64 idx,
+                 Buffer<enc> out1, Buffer<enc> out2, Buffer<enc> out3,
+                 npy_intp *final_len1, npy_intp *final_len2, npy_intp *final_len3,
+                 STARTPOSITION pos)
+{
+    size_t len1 = buf1.num_codepoints();
+    size_t len2 = buf2.num_codepoints();
+
+    if (len2 == 0) {
+        npy_gil_error(PyExc_ValueError, "empty separator");
+        *final_len1 = *final_len2 = *final_len3 = -1;
+        return;
+    }
+
+    if (idx < 0) {
+        if (pos == STARTPOSITION::FRONT) {
+            buf1.buffer_memcpy(out1, len1);
+            *final_len1 = len1;
+            *final_len2 = *final_len3 = 0;
+        }
+        else {
+            buf1.buffer_memcpy(out3, len1);
+            *final_len1 = *final_len2 = 0;
+            *final_len3 = len;
+        }
+        return;
+    }
+
+    buf1.buffer_memcpy(out1, idx);
+    *final_len1 = idx;
+    buf2.buffer_memcpy(out2, len2);
+    *final_len2 = len2;
+    (buf1 + idx + len2).buffer_memcpy(out3, len1 - idx - len2);
+    *final_len3 = len1 - idx - len2;
+}
+
+
 #endif /* _NPY_CORE_SRC_UMATH_STRING_BUFFER_H_ */
diff --git a/numpy/_core/src/umath/string_ufuncs.cpp b/numpy/_core/src/umath/string_ufuncs.cpp
index 337c8f65ba1e..8968f915fd3e 100644
--- a/numpy/_core/src/umath/string_ufuncs.cpp
+++ b/numpy/_core/src/umath/string_ufuncs.cpp
@@ -582,6 +582,57 @@ string_zfill_loop(PyArrayMethod_Context *context,
 }
 
 
+template <ENCODING enc>
+static int
+string_partition_loop(PyArrayMethod_Context *context,
+        char *const data[], npy_intp const dimensions[],
+        npy_intp const strides[], NpyAuxData *NPY_UNUSED(auxdata))
+{
+    STARTPOSITION startposition = *(STARTPOSITION *)(context->method->static_data);
+    int elsize1 = context->descriptors[0]->elsize;
+    int elsize2 = context->descriptors[1]->elsize;
+    int outsize1 = context->descriptors[3]->elsize;
+    int outsize2 = context->descriptors[4]->elsize;
+    int outsize3 = context->descriptors[5]->elsize;
+
+    char *in1 = data[0];
+    char *in2 = data[1];
+    char *in3 = data[2];
+    char *out1 = data[3];
+    char *out2 = data[4];
+    char *out3 = data[5];
+
+    npy_intp N = dimensions[0];
+
+    while (N--) {
+        Buffer<enc> buf1(in1, elsize1);
+        Buffer<enc> buf2(in2, elsize2);
+        Buffer<enc> outbuf1(out1, outsize1);
+        Buffer<enc> outbuf2(out2, outsize2);
+        Buffer<enc> outbuf3(out3, outsize3);
+
+        npy_intp final_len1, final_len2, final_len3;
+        string_partition(buf1, buf2, *(npy_int64 *)in3, outbuf1, outbuf2, outbuf3,
+                         &final_len1, &final_len2, &final_len3, startposition);
+        if (final_len1 < 0 || final_len2 < 0 || final_len3 < 0) {
+            return -1;
+        }
+        outbuf1.buffer_fill_with_zeros_after_index(final_len1);
+        outbuf2.buffer_fill_with_zeros_after_index(final_len2);
+        outbuf3.buffer_fill_with_zeros_after_index(final_len3);
+
+        in1 += strides[0];
+        in2 += strides[1];
+        in3 += strides[2];
+        out1 += strides[3];
+        out2 += strides[4];
+        out3 += strides[5];
+    }
+
+    return 0;
+}
+
+
 /* Resolve descriptors & promoter functions */
 
 static NPY_CASTING
@@ -947,6 +998,91 @@ string_zfill_resolve_descriptors(
 }
 
 
+static int
+string_partition_promoter(PyObject *NPY_UNUSED(ufunc),
+        PyArray_DTypeMeta *op_dtypes[], PyArray_DTypeMeta *signature[],
+        PyArray_DTypeMeta *new_op_dtypes[])
+{
+    Py_INCREF(op_dtypes[0]);
+    new_op_dtypes[0] = op_dtypes[0];
+    Py_INCREF(op_dtypes[1]);
+    new_op_dtypes[1] = op_dtypes[1];
+
+    new_op_dtypes[2] = NPY_DT_NewRef(&PyArray_Int64DType);
+
+    Py_INCREF(op_dtypes[3]);
+    new_op_dtypes[3] = op_dtypes[3];
+    Py_INCREF(op_dtypes[4]);
+    new_op_dtypes[4] = op_dtypes[4];
+    Py_INCREF(op_dtypes[5]);
+    new_op_dtypes[5] = op_dtypes[5];
+    return 0;
+}
+
+
+static NPY_CASTING
+string_partition_resolve_descriptors(
+        PyArrayMethodObject *NPY_UNUSED(self),
+        PyArray_DTypeMeta *NPY_UNUSED(dtypes[3]),
+        PyArray_Descr *given_descrs[3],
+        PyArray_Descr *loop_descrs[3],
+        npy_intp *NPY_UNUSED(view_offset))
+{
+    if (given_descrs[3] == NULL) {
+        PyErr_SetString(
+            PyExc_TypeError,
+            "The 'out' kwarg is necessary. Use numpy.strings without it.");
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    if (given_descrs[4] == NULL) {
+        PyErr_SetString(
+            PyExc_TypeError,
+            "The 'out' kwarg is necessary. Use numpy.strings without it.");
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    if (given_descrs[5] == NULL) {
+        PyErr_SetString(
+            PyExc_TypeError,
+            "The 'out' kwarg is necessary. Use numpy.strings without it.");
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    loop_descrs[0] = NPY_DT_CALL_ensure_canonical(given_descrs[0]);
+    if (loop_descrs[0] == NULL) {
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    loop_descrs[1] = NPY_DT_CALL_ensure_canonical(given_descrs[1]);
+    if (loop_descrs[1] == NULL) {
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    loop_descrs[2] = NPY_DT_CALL_ensure_canonical(given_descrs[2]);
+    if (loop_descrs[2] == NULL) {
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    loop_descrs[3] = NPY_DT_CALL_ensure_canonical(given_descrs[3]);
+    if (loop_descrs[3] == NULL) {
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    loop_descrs[4] = NPY_DT_CALL_ensure_canonical(given_descrs[4]);
+    if (loop_descrs[4] == NULL) {
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    loop_descrs[5] = NPY_DT_CALL_ensure_canonical(given_descrs[5]);
+    if (loop_descrs[5] == NULL) {
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    return NPY_NO_CASTING;
+}
+
+
 /*
  * Machinery to add the string loops to the existing ufuncs.
  */
@@ -1228,7 +1364,7 @@ init_mixed_type_ufunc(PyObject *umath, const char *name, int nin, int nout,
 NPY_NO_EXPORT int
 init_string_ufuncs(PyObject *umath)
 {
-    NPY_TYPES dtypes[] = {NPY_STRING, NPY_STRING, NPY_STRING, NPY_STRING, NPY_STRING};
+    NPY_TYPES dtypes[] = {NPY_STRING, NPY_STRING, NPY_STRING, NPY_STRING, NPY_STRING, NPY_STRING};
 
     if (init_comparison(umath) < 0) {
         return -1;
@@ -1599,6 +1735,34 @@ init_string_ufuncs(PyObject *umath)
         return -1;
     }
 
+    dtypes[0] = dtypes[1] = dtypes[3] = dtypes[4] = dtypes[5] = NPY_OBJECT;
+    dtypes[2] = NPY_INT64;
+
+    const char *partition_names[] = {"_partition", "_rpartition"};
+
+    static STARTPOSITION partition_startpositions[] = {
+        STARTPOSITION::FRONT, STARTPOSITION::BACK
+    };
+
+    for (int i = 0; i < 2; i++) {
+        if (init_ufunc(
+                umath, partition_names[i], 3, 3, dtypes, ENCODING::ASCII,
+                string_partition_loop<ENCODING::ASCII>,
+                string_partition_resolve_descriptors, &partition_startpositions[i]) < 0) {
+            return -1;
+        }
+        if (init_ufunc(
+                umath, partition_names[i], 3, 3, dtypes, ENCODING::UTF32,
+                string_partition_loop<ENCODING::UTF32>,
+                string_partition_resolve_descriptors, &partition_startpositions[i]) < 0) {
+            return -1;
+        }
+        if (init_promoter(umath, partition_names[i], 3, 3,
+                string_partition_promoter) < 0) {
+            return -1;
+        }
+    }
+
     return 0;
 }
 
diff --git a/numpy/_core/strings.py b/numpy/_core/strings.py
index c79c7db494ff..849eb3da074b 100644
--- a/numpy/_core/strings.py
+++ b/numpy/_core/strings.py
@@ -41,6 +41,8 @@
     _ljust,
     _rjust,
     _zfill,
+    _partition,
+    _rpartition,
 )
 
 
@@ -51,7 +53,7 @@
     "isupper", "istitle", "isdecimal", "isnumeric", "str_len", "find",
     "rfind", "index", "rindex", "count", "startswith", "endswith", "lstrip",
     "rstrip", "strip", "replace", "expandtabs", "center", "ljust", "rjust",
-    "zfill",
+    "zfill", "partition", "rpartition",
 
     # _vec_string - Will gradually become ufuncs as well
     "upper", "lower", "swapcase", "capitalize", "title",
@@ -60,7 +62,7 @@
     "mod", "decode", "encode", "translate",
 
     # Removed from namespace until behavior has been crystalized
-    # "join", "split", "rsplit", "splitlines", "partition", "rpartition",
+    # "join", "split", "rsplit", "splitlines",
 ]
 
 
@@ -1315,72 +1317,96 @@ def _splitlines(a, keepends=None):
         a, np.object_, 'splitlines', _clean_args(keepends))
 
 
-def _partition(a, sep):
+def partition(a, sep):
     """
-    Partition each element in `a` around `sep`.
+    Partition each element in ``a`` around ``sep``.
 
-    Calls :meth:`str.partition` element-wise.
-
-    For each element in `a`, split the element as the first
-    occurrence of `sep`, and return 3 strings containing the part
+    For each element in ``a``, split the element at the first
+    occurrence of ``sep``, and return a 3-tuple containing the part
     before the separator, the separator itself, and the part after
-    the separator. If the separator is not found, return 3 strings
-    containing the string itself, followed by two empty strings.
+    the separator. If the separator is not found, the first item of
+    the tuple will contain the whole string, and the second and third
+    ones will be the empty string.
 
     Parameters
     ----------
     a : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
         Input array
-    sep : {str, unicode}
-        Separator to split each string element in `a`.
+    sep : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Separator to split each string element in ``a``.
 
     Returns
     -------
-    out : ndarray
-        Output array of ``StringDType``, ``bytes_`` or ``str_`` dtype,
-        depending on input types. The output array will have an extra
-        dimension with 3 elements per input element.
+    out : 3-tuple:
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part before the separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part after the separator
 
-    Examples
-    --------
-    >>> x = np.array(["Numpy is nice!"])
-    >>> np.strings.partition(x, " ")  # doctest: +SKIP
-    array([['Numpy', ' ', 'is nice!']], dtype='<U8')  # doctest: +SKIP
-    
     See Also
     --------
     str.partition
 
+    Examples
+    --------
+    >>> x = np.array(["Numpy is nice!"])
+    >>> np.strings.partition(x, " ")
+    (array(['Numpy'], dtype='<U5'),
+     array([ True]),
+     array(['is nice!'], dtype='<U8'))
+
     """
-    return _to_bytes_or_str_array(
-        _vec_string(a, np.object_, 'partition', (sep,)), a)
+    a = np.asanyarray(a)
+    sep = np.asanyarray(sep)
 
+    shape = np.broadcast_shapes(a.shape, sep.shape)
+    pos = _find_ufunc(a, sep, 0, MAX)
+
+    a_len = str_len(a)
+    sep_len = str_len(sep)
 
-def _rpartition(a, sep):
+    not_found = pos < 0
+    buffersizes1 = np.where(not_found, a_len, pos)
+    buffersizes3 = np.where(not_found, 0, a_len - pos - sep_len)
+
+    out_dtype1 = f"{a.dtype.char}{buffersizes1.max()}"
+    out1 = np.empty_like(a, shape=shape, dtype=out_dtype1)
+    out_dtype2 = f"{a.dtype.char}{0 if np.all(not_found) else sep_len.max()}"
+    out2 = np.empty_like(a, shape=shape, dtype=out_dtype2)
+    out_dtype3 = f"{a.dtype.char}{buffersizes3.max()}"
+    out3 = np.empty_like(a, shape=shape, dtype=out_dtype3)
+    return _partition(a, sep, pos, out=(out1, out2, out3))
+
+
+def rpartition(a, sep):
     """
     Partition (split) each element around the right-most separator.
 
-    Calls :meth:`str.rpartition` element-wise.
-
-    For each element in `a`, split the element as the last
-    occurrence of `sep`, and return 3 strings containing the part
+    For each element in ``a``, split the element at the last
+    occurrence of ``sep``, and return a 3-tuple containing the part
     before the separator, the separator itself, and the part after
-    the separator. If the separator is not found, return 3 strings
-    containing the string itself, followed by two empty strings.
+    the separator. If the separator is not found, the third item of
+    the tuple will contain the whole string, and the first and second
+    ones will be the empty string.
 
     Parameters
     ----------
     a : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
         Input array
-    sep : str or unicode
-        Right-most separator to split each element in array.
+    sep : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Separator to split each string element in ``a``.
 
     Returns
     -------
-    out : ndarray
-        Output array of ``StringDType``, ``bytes_`` or ``str_`` dtype,
-        depending on input types. The output array will have an extra
-        dimension with 3 elements per input element.
+    out : 3-tuple:
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part before the separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part after the separator
 
     See Also
     --------
@@ -1389,14 +1415,33 @@ def _rpartition(a, sep):
     Examples
     --------
     >>> a = np.array(['aAaAaA', '  aA  ', 'abBABba'])
-    >>> np.strings.rpartition(a, 'A')  # doctest: +SKIP
-    array([['aAaAa', 'A', ''],  # doctest: +SKIP
-       ['  a', 'A', '  '],  # doctest: +SKIP
-       ['abB', 'A', 'Bba']], dtype='<U5')  # doctest: +SKIP
+    >>> np.strings.rpartition(a, 'A')
+    (array(['aAaAa', '  a', 'abB'], dtype='<U5'),
+     array([ True,  True,  True]),
+     array(['', '  ', 'Bba'], dtype='<U3'))
 
     """
-    return _to_bytes_or_str_array(
-        _vec_string(a, np.object_, 'rpartition', (sep,)), a)
+    a = np.asanyarray(a)
+    sep = np.asanyarray(sep)
+
+    shape = np.broadcast_shapes(a.shape, sep.shape)
+    pos = _rfind_ufunc(a, sep, 0, MAX)
+
+    a_len = str_len(a)
+    sep_len = str_len(sep)
+
+    not_found = pos < 0
+    buffersizes1 = np.where(not_found, 0, pos)
+    buffersizes3 = np.where(not_found, a_len, a_len - pos - sep_len)
+
+    out_dtype1 = f"{a.dtype.char}{buffersizes1.max()}"
+    out1 = np.empty_like(a, shape=shape, dtype=out_dtype1)
+    out_dtype2 = f"{a.dtype.char}{0 if np.all(not_found) else sep_len.max()}"
+    out2 = np.empty_like(a, shape=shape, dtype=out_dtype2)
+    out_dtype3 = f"{a.dtype.char}{buffersizes3.max()}"
+    out3 = np.empty_like(a, shape=shape, dtype=out_dtype3)
+
+    return _rpartition(a, sep, pos, out=(out1, out2, out3))
 
 
 def translate(a, table, deletechars=None):
diff --git a/numpy/_core/tests/test_strings.py b/numpy/_core/tests/test_strings.py
index 25d605d2ce6e..fe62530ea538 100644
--- a/numpy/_core/tests/test_strings.py
+++ b/numpy/_core/tests/test_strings.py
@@ -796,6 +796,54 @@ def test_zfill(self, buf, width, res, dt):
         assert_array_equal(np.strings.zfill(buf, width), res)
 
 
+@pytest.mark.parametrize("dt", [
+    "U",
+    "S",
+    pytest.param("T", marks=pytest.mark.xfail(
+        reason="StringDType support not implemented",
+        strict=True),
+    ),
+])
+class TestMethodsWithoutStringDTypeSupport:
+    @pytest.mark.parametrize("buf,sep,res1,res2,res3", [
+        ("this is the partition method", "ti", "this is the par",
+            "ti", "tion method"),
+        ("http://www.python.org", "://", "http", "://", "www.python.org"),
+        ("http://www.python.org", "?", "http://www.python.org", "", ""),
+        ("http://www.python.org", "http://", "", "http://", "www.python.org"),
+        ("http://www.python.org", "org", "http://www.python.", "org", ""),
+        ("mississippi", "ss", "mi", "ss", "issippi"),
+        ("mississippi", "i", "m", "i", "ssissippi"),
+        ("mississippi", "w", "mississippi", "", ""),
+    ])
+    def test_partition(self, buf, sep, res1, res2, res3, dt):
+        buf = np.array(buf, dtype=dt)
+        sep = np.array(sep, dtype=dt)
+        res1 = np.array(res1, dtype=dt)
+        res2 = np.array(res2, dtype=dt)
+        res3 = np.array(res3, dtype=dt)
+        assert_array_equal(np.strings.partition(buf, sep), (res1, res2, res3))
+
+    @pytest.mark.parametrize("buf,sep,res1,res2,res3", [
+        ("this is the partition method", "ti", "this is the parti",
+            "ti", "on method"),
+        ("http://www.python.org", "://", "http", "://", "www.python.org"),
+        ("http://www.python.org", "?", "", "", "http://www.python.org"),
+        ("http://www.python.org", "http://", "", "http://", "www.python.org"),
+        ("http://www.python.org", "org", "http://www.python.", "org", ""),
+        ("mississippi", "ss", "missi", "ss", "ippi"),
+        ("mississippi", "i", "mississipp", "i", ""),
+        ("mississippi", "w", "", "", "mississippi"),
+    ])
+    def test_rpartition(self, buf, sep, res1, res2, res3, dt):
+        buf = np.array(buf, dtype=dt)
+        sep = np.array(sep, dtype=dt)
+        res1 = np.array(res1, dtype=dt)
+        res2 = np.array(res2, dtype=dt)
+        res3 = np.array(res3, dtype=dt)
+        assert_array_equal(np.strings.rpartition(buf, sep), (res1, res2, res3))
+
+
 @pytest.mark.parametrize("dt", ["U", "T"])
 class TestMethodsWithUnicode:
     @pytest.mark.parametrize("in_,out", [
@@ -969,6 +1017,47 @@ def test_rjust(self, buf, width, fillchar, res, dt):
         assert_array_equal(np.strings.rjust(buf, width, fillchar), res)
 
 
+@pytest.mark.parametrize("dt", [
+    "U",
+    pytest.param("T", marks=pytest.mark.xfail(
+        reason="StringDType support not implemented",
+        strict=True),
+    ),
+])
+class TestMethodsWithUnicodeWithoutStringDTypeSupport:
+    @pytest.mark.parametrize("buf,sep,res1,res2,res3", [
+        ("āāāāĀĀĀĀ", "Ă", "āāāāĀĀĀĀ", "", ""),
+        ("āāāāĂĀĀĀĀ", "Ă", "āāāā", "Ă", "ĀĀĀĀ"),
+        ("āāāāĂĂĀĀĀĀ", "ĂĂ", "āāāā", "ĂĂ", "ĀĀĀĀ"),
+        ("𐌁𐌁𐌁𐌁𐌀𐌀𐌀𐌀", "𐌂", "𐌁𐌁𐌁𐌁𐌀𐌀𐌀𐌀", "", ""),
+        ("𐌁𐌁𐌁𐌁𐌂𐌀𐌀𐌀𐌀", "𐌂", "𐌁𐌁𐌁𐌁", "𐌂", "𐌀𐌀𐌀𐌀"),
+        ("𐌁𐌁𐌁𐌁𐌂𐌂𐌀𐌀𐌀𐌀", "𐌂𐌂", "𐌁𐌁𐌁𐌁", "𐌂𐌂", "𐌀𐌀𐌀𐌀"),
+    ])
+    def test_partition(self, buf, sep, res1, res2, res3, dt):
+        buf = np.array(buf, dtype=dt)
+        sep = np.array(sep, dtype=dt)
+        res1 = np.array(res1, dtype=dt)
+        res2 = np.array(res2, dtype=dt)
+        res3 = np.array(res3, dtype=dt)
+        assert_array_equal(np.strings.partition(buf, sep), (res1, res2, res3))
+
+    @pytest.mark.parametrize("buf,sep,res1,res2,res3", [
+        ("āāāāĀĀĀĀ", "Ă", "", "", "āāāāĀĀĀĀ"),
+        ("āāāāĂĀĀĀĀ", "Ă", "āāāā", "Ă", "ĀĀĀĀ"),
+        ("āāāāĂĂĀĀĀĀ", "ĂĂ", "āāāā", "ĂĂ", "ĀĀĀĀ"),
+        ("𐌁𐌁𐌁𐌁𐌀𐌀𐌀𐌀", "𐌂", "", "", "𐌁𐌁𐌁𐌁𐌀𐌀𐌀𐌀"),
+        ("𐌁𐌁𐌁𐌁𐌂𐌀𐌀𐌀𐌀", "𐌂", "𐌁𐌁𐌁𐌁", "𐌂", "𐌀𐌀𐌀𐌀"),
+        ("𐌁𐌁𐌁𐌁𐌂𐌂𐌀𐌀𐌀𐌀", "𐌂𐌂", "𐌁𐌁𐌁𐌁", "𐌂𐌂", "𐌀𐌀𐌀𐌀"),
+    ])
+    def test_rpartition(self, buf, sep, res1, res2, res3, dt):
+        buf = np.array(buf, dtype=dt)
+        sep = np.array(sep, dtype=dt)
+        res1 = np.array(res1, dtype=dt)
+        res2 = np.array(res2, dtype=dt)
+        res3 = np.array(res3, dtype=dt)
+        assert_array_equal(np.strings.rpartition(buf, sep), (res1, res2, res3))
+
+
 class TestMixedTypeMethods:
     def test_center(self):
         buf = np.array("😊", dtype="U")
diff --git a/numpy/_core/umath.py b/numpy/_core/umath.py
index db26900c00b8..216d4196748d 100644
--- a/numpy/_core/umath.py
+++ b/numpy/_core/umath.py
@@ -19,7 +19,7 @@
 from ._multiarray_umath import (
     _replace, _strip_whitespace, _lstrip_whitespace, _rstrip_whitespace,
     _strip_chars, _lstrip_chars, _rstrip_chars, _expandtabs_length,
-    _expandtabs, _center, _ljust, _rjust, _zfill)
+    _expandtabs, _center, _ljust, _rjust, _zfill, _partition, _rpartition)
 
 __all__ = [
     'absolute', 'add',


Code review comments:
diff_hunk: @@ -5028,3 +5028,91 @@ def add_newdoc(place, name, doc):
     array(['001', '-01', '+01'], dtype='<U3')
 
     """)
+
+add_newdoc('numpy._core.umath', '_partition',
+    """
+    Partition each element in ``x1`` around ``x2``.
+
+    For each element in ``x1``, split the element at the first
+    occurrence of ``x2``, and return a 3-tuple containing the part
+    before the separator, the separator itself, and the part after
+    the separator. If the separator is not found, the first item of
+    the tuple will contain the whole string, and the second and third
+    ones will be the empty string.
+
+    Parameters
+    ----------
+    x1 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Input array
+    x2 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Separator to split each string element in ``x1``.
+    x3 : array-like, with any integer dtype
+        The indices of the separator
path: numpy/_core/code_generators/ufunc_docstrings.py
text: @user1:
Add "The indices of the separator (<0 to indicate the separator is not present)."

diff_hunk: @@ -1315,72 +1317,96 @@ def _splitlines(a, keepends=None):
         a, np.object_, 'splitlines', _clean_args(keepends))
 
 
-def _partition(a, sep):
+def partition(a, sep):
     """
-    Partition each element in `a` around `sep`.
+    Partition each element in ``a`` around ``sep``.
 
-    Calls :meth:`str.partition` element-wise.
-
-    For each element in `a`, split the element as the first
-    occurrence of `sep`, and return 3 strings containing the part
+    For each element in ``a``, split the element at the first
+    occurrence of ``sep``, and return a 3-tuple containing the part
     before the separator, the separator itself, and the part after
-    the separator. If the separator is not found, return 3 strings
-    containing the string itself, followed by two empty strings.
+    the separator. If the separator is not found, the first item of
+    the tuple will contain the whole string, and the second and third
+    ones will be the empty string.
 
     Parameters
     ----------
     a : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
         Input array
-    sep : {str, unicode}
-        Separator to split each string element in `a`.
+    sep : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Separator to split each string element in ``a``.
 
     Returns
     -------
-    out : ndarray
-        Output array of ``StringDType``, ``bytes_`` or ``str_`` dtype,
-        depending on input types. The output array will have an extra
-        dimension with 3 elements per input element.
+    out : 3-tuple:
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part before the separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part after the separator
 
-    Examples
-    --------
-    >>> x = np.array(["Numpy is nice!"])
-    >>> np.strings.partition(x, " ")  # doctest: +SKIP
-    array([['Numpy', ' ', 'is nice!']], dtype='<U8')  # doctest: +SKIP
-    
     See Also
     --------
     str.partition
 
+    Examples
+    --------
+    >>> x = np.array(["Numpy is nice!"])
+    >>> np.strings.partition(x, " ")
+    (array(['Numpy'], dtype='<U5'),
+     array([ True]),
+     array(['is nice!'], dtype='<U8'))
+
     """
-    return _to_bytes_or_str_array(
-        _vec_string(a, np.object_, 'partition', (sep,)), a)
+    a = np.asanyarray(a)
+    sep = np.asanyarray(sep)
 
+    shape = np.broadcast_shapes(a.shape, sep.shape)
+    pos = _find_ufunc(a, sep, 0, MAX)
+
+    a_len = str_len(a)
+    sep_len = str_len(sep)
 
-def _rpartition(a, sep):
+    not_found = pos < 0
+    buffersizes1 = np.where(not_found, a_len, pos)
+    buffersizes3 = np.where(not_found, 0, a_len - pos - sep_len)
+
+    out_dtype1 = f"{a.dtype.char}{buffersizes1.max()}"
path: numpy/_core/strings.py
text: @user1:
I think one could get a bit of a speed-up for large arrays by keeping the strings together, as follows,
```
out_dtype = ",".join(
    [f"{a.dtype.char}{n}" for n in (
        buffersizes1.max(),
        0 if np.all(not_found else sel_lep.max(),
        buffersizes3.max()
    )] 
out = np.empty_like(a, shape=shape, dtype=out_dtype)
return _partition(a, sep, pos out=(out["f0"], out["f1"], out["f2"]))
```
Though perhaps not a big effect, and maybe more useful for the `np.char` version -- see there.


@author:
Done.

diff_hunk: @@ -947,6 +998,91 @@ string_zfill_resolve_descriptors(
 }
 
 
+static int
+string_partition_promoter(PyObject *NPY_UNUSED(ufunc),
+        PyArray_DTypeMeta *op_dtypes[], PyArray_DTypeMeta *signature[],
+        PyArray_DTypeMeta *new_op_dtypes[])
+{
+    Py_INCREF(op_dtypes[0]);
+    new_op_dtypes[0] = op_dtypes[0];
+    Py_INCREF(op_dtypes[1]);
+    new_op_dtypes[1] = op_dtypes[1];
+
+    new_op_dtypes[2] = NPY_DT_NewRef(&PyArray_Int64DType);
+
+    Py_INCREF(op_dtypes[3]);
+    new_op_dtypes[3] = op_dtypes[3];
+    Py_INCREF(op_dtypes[4]);
+    new_op_dtypes[4] = op_dtypes[4];
+    Py_INCREF(op_dtypes[5]);
+    new_op_dtypes[5] = op_dtypes[5];
+    return 0;
+}
+
+
+static NPY_CASTING
+string_partition_resolve_descriptors(
+        PyArrayMethodObject *NPY_UNUSED(self),
+        PyArray_DTypeMeta *NPY_UNUSED(dtypes[3]),
+        PyArray_Descr *given_descrs[3],
+        PyArray_Descr *loop_descrs[3],
+        npy_intp *NPY_UNUSED(view_offset))
+{
+    if (given_descrs[3] == NULL) {
path: numpy/_core/src/umath/string_ufuncs.cpp
text: @user1:
Can we combine the three checks on the output? I.e., `if (given_descrs[3] == NULL || give...)`. Or make it a loop.

diff_hunk: @@ -947,6 +998,91 @@ string_zfill_resolve_descriptors(
 }
 
 
+static int
+string_partition_promoter(PyObject *NPY_UNUSED(ufunc),
+        PyArray_DTypeMeta *op_dtypes[], PyArray_DTypeMeta *signature[],
+        PyArray_DTypeMeta *new_op_dtypes[])
+{
+    Py_INCREF(op_dtypes[0]);
+    new_op_dtypes[0] = op_dtypes[0];
+    Py_INCREF(op_dtypes[1]);
+    new_op_dtypes[1] = op_dtypes[1];
+
+    new_op_dtypes[2] = NPY_DT_NewRef(&PyArray_Int64DType);
+
+    Py_INCREF(op_dtypes[3]);
+    new_op_dtypes[3] = op_dtypes[3];
+    Py_INCREF(op_dtypes[4]);
+    new_op_dtypes[4] = op_dtypes[4];
+    Py_INCREF(op_dtypes[5]);
+    new_op_dtypes[5] = op_dtypes[5];
+    return 0;
+}
+
+
+static NPY_CASTING
+string_partition_resolve_descriptors(
+        PyArrayMethodObject *NPY_UNUSED(self),
+        PyArray_DTypeMeta *NPY_UNUSED(dtypes[3]),
+        PyArray_Descr *given_descrs[3],
+        PyArray_Descr *loop_descrs[3],
+        npy_intp *NPY_UNUSED(view_offset))
+{
+    if (given_descrs[3] == NULL) {
+        PyErr_SetString(
+            PyExc_TypeError,
+            "The 'out' kwarg is necessary. Use numpy.strings without it.");
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    if (given_descrs[4] == NULL) {
+        PyErr_SetString(
+            PyExc_TypeError,
+            "The 'out' kwarg is necessary. Use numpy.strings without it.");
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    if (given_descrs[5] == NULL) {
+        PyErr_SetString(
+            PyExc_TypeError,
+            "The 'out' kwarg is necessary. Use numpy.strings without it.");
+        return _NPY_ERROR_OCCURRED_IN_CAST;
+    }
+
+    loop_descrs[0] = NPY_DT_CALL_ensure_canonical(given_descrs[0]);
path: numpy/_core/src/umath/string_ufuncs.cpp
text: @user1:
Here, a loop would seem fine, `for (i = 0, i < 6, i++) {`

diff_hunk: @@ -1315,72 +1317,96 @@ def _splitlines(a, keepends=None):
         a, np.object_, 'splitlines', _clean_args(keepends))
 
 
-def _partition(a, sep):
+def partition(a, sep):
     """
-    Partition each element in `a` around `sep`.
+    Partition each element in ``a`` around ``sep``.
 
-    Calls :meth:`str.partition` element-wise.
-
-    For each element in `a`, split the element as the first
-    occurrence of `sep`, and return 3 strings containing the part
+    For each element in ``a``, split the element at the first
+    occurrence of ``sep``, and return a 3-tuple containing the part
     before the separator, the separator itself, and the part after
-    the separator. If the separator is not found, return 3 strings
-    containing the string itself, followed by two empty strings.
+    the separator. If the separator is not found, the first item of
+    the tuple will contain the whole string, and the second and third
+    ones will be the empty string.
 
     Parameters
     ----------
     a : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
         Input array
-    sep : {str, unicode}
-        Separator to split each string element in `a`.
+    sep : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Separator to split each string element in ``a``.
 
     Returns
     -------
-    out : ndarray
-        Output array of ``StringDType``, ``bytes_`` or ``str_`` dtype,
-        depending on input types. The output array will have an extra
-        dimension with 3 elements per input element.
+    out : 3-tuple:
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part before the separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part after the separator
 
-    Examples
-    --------
-    >>> x = np.array(["Numpy is nice!"])
-    >>> np.strings.partition(x, " ")  # doctest: +SKIP
-    array([['Numpy', ' ', 'is nice!']], dtype='<U8')  # doctest: +SKIP
-    
     See Also
     --------
     str.partition
 
+    Examples
+    --------
+    >>> x = np.array(["Numpy is nice!"])
+    >>> np.strings.partition(x, " ")
+    (array(['Numpy'], dtype='<U5'),
+     array([ True]),
path: numpy/_core/strings.py
text: @user1:
`True` -> `' '`

diff_hunk: @@ -5028,3 +5028,91 @@ def add_newdoc(place, name, doc):
     array(['001', '-01', '+01'], dtype='<U3')
 
     """)
+
+add_newdoc('numpy._core.umath', '_partition',
+    """
+    Partition each element in ``x1`` around ``x2``.
+
+    For each element in ``x1``, split the element at the first
+    occurrence of ``x2``, and return a 3-tuple containing the part
+    before the separator, the separator itself, and the part after
+    the separator. If the separator is not found, the first item of
+    the tuple will contain the whole string, and the second and third
+    ones will be the empty string.
+
+    Parameters
+    ----------
+    x1 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Input array
+    x2 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Separator to split each string element in ``x1``.
+    x3 : array-like, with any integer dtype
+        The indices of the separator
+
+    Returns
+    -------
+    out : 3-tuple:
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part before the separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part after the separator
+
+    See Also
+    --------
+    str.partition
+
+    Examples
+    --------
+    >>> x = np.array(["Numpy is nice!"])
+    >>> np.strings.partition(x, " ")
+    (array(['Numpy'], dtype='<U5'),
+     array([ True]),
+     array(['is nice!'], dtype='<U8'))
+
+    """)
+
+add_newdoc('numpy._core.umath', '_rpartition',
+    """
+    Partition (split) each element around the right-most separator.
+
+    For each element in ``x1``, split the element at the last
+    occurrence of ``x2``, and return a 3-tuple containing the part
+    before the separator, the separator itself, and the part after
+    the separator. If the separator is not found, the third item of
+    the tuple will contain the whole string, and the first and second
+    ones will be the empty string.
+
+    Parameters
+    ----------
+    x1 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Input array
+    x2 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Separator to split each string element in ``x1``.
+    x3 : array-like, with any integer dtype
+        The indices of the separator
+
+    Returns
+    -------
+    out : 3-tuple:
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part before the separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part after the separator
+
+    See Also
+    --------
+    str.rpartition
+
+    Examples
+    --------
+    >>> a = np.array(['aAaAaA', '  aA  ', 'abBABba'])
+    >>> np.strings.rpartition(a, 'A')
+    (array(['aAaAa', '  a', 'abB'], dtype='<U5'),
+     array([ True,  True,  True]),
path: numpy/_core/code_generators/ufunc_docstrings.py
text: @user1:
'A' instead of `True`

diff_hunk: @@ -796,6 +796,54 @@ def test_zfill(self, buf, width, res, dt):
         assert_array_equal(np.strings.zfill(buf, width), res)
 
 
+@pytest.mark.parametrize("dt", [
+    "U",
+    "S",
+    pytest.param("T", marks=pytest.mark.xfail(
+        reason="StringDType support not implemented",
+        strict=True),
+    ),
+])
+class TestMethodsWithoutStringDTypeSupport:
+    @pytest.mark.parametrize("buf,sep,res1,res2,res3", [
+        ("this is the partition method", "ti", "this is the par",
+            "ti", "tion method"),
+        ("http://www.python.org", "://", "http", "://", "www.python.org"),
+        ("http://www.python.org", "?", "http://www.python.org", "", ""),
+        ("http://www.python.org", "http://", "", "http://", "www.python.org"),
+        ("http://www.python.org", "org", "http://www.python.", "org", ""),
+        ("mississippi", "ss", "mi", "ss", "issippi"),
+        ("mississippi", "i", "m", "i", "ssissippi"),
+        ("mississippi", "w", "mississippi", "", ""),
+    ])
+    def test_partition(self, buf, sep, res1, res2, res3, dt):
+        buf = np.array(buf, dtype=dt)
+        sep = np.array(sep, dtype=dt)
+        res1 = np.array(res1, dtype=dt)
+        res2 = np.array(res2, dtype=dt)
+        res3 = np.array(res3, dtype=dt)
+        assert_array_equal(np.strings.partition(buf, sep), (res1, res2, res3))
path: numpy/_core/tests/test_strings.py
text: @user1:
The tuple does an implicit conversion to a new array. I'd write 
```
p1, p2, p3 = np.strings.partition(buf, sep)
assert_array_equal(p1, res1)
assert_array_equal(p2, res2)
assert_array_equal(p3, res3)
assert_array_equal(p1 + p2 + p3, buf)
```
Actually not 100% sure the latter works...

diff_hunk: @@ -303,6 +303,90 @@ def multiply(a, i):
         raise ValueError("Can only multiply by integers")
 
 
+def partition(a, sep):
+    """
+    Partition each element in `a` around `sep`.
+
+    Calls :meth:`str.partition` element-wise.
+
+    For each element in `a`, split the element as the first
+    occurrence of `sep`, and return 3 strings containing the part
+    before the separator, the separator itself, and the part after
+    the separator. If the separator is not found, return 3 strings
+    containing the string itself, followed by two empty strings.
+
+    Parameters
+    ----------
+    a : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Input array
+    sep : {str, unicode}
+        Separator to split each string element in `a`.
+
+    Returns
+    -------
+    out : ndarray
+        Output array of ``StringDType``, ``bytes_`` or ``str_`` dtype,
+        depending on input types. The output array will have an extra
+        dimension with 3 elements per input element.
+
+    Examples
+    --------
+    >>> x = np.array(["Numpy is nice!"])
+    >>> np.char.partition(x, " ")
+    array([['Numpy', ' ', 'is nice!']], dtype='<U8')
+    
+    See Also
+    --------
+    str.partition
+
+    """
+    return _to_bytes_or_str_array(
path: numpy/_core/defchararray.py
text: @user1:
Maybe not worth it, but in principle one could use 
```
return np.stack(*np.strings.partition(a, sep), axis=-1)
```
Alternatively, one can follow along the lines of what I suggest below for the `np.strings` function, to create an `out` array that contains all three, but with equal-sized entries, and pass in a tuple that contains views, like
```
n = max(buffersizes1.max(), buffersizes3.max())  # sep_len.max() guaranteed to be smaller.
out = np.empty_like(a, shape=shape, dtype=f"(3,){a.dtype.char}{n}")
return _partition(a, sep, pos out=tuple(np.moveaxis(out, -1, 0)))
```
(could be fairly easily combined with the `np.strings` version with some convenience function).

@author:
I like the conciseness of the `np.stack` approach. I think the goal for the `np.char` wrappers should be to be as thin as possible, while keeping backwards-compatibility.

@user1:
Sounds good!

diff_hunk: @@ -5028,3 +5028,91 @@ def add_newdoc(place, name, doc):
     array(['001', '-01', '+01'], dtype='<U3')
 
     """)
+
+add_newdoc('numpy._core.umath', '_partition',
+    """
+    Partition each element in ``x1`` around ``x2``.
+
+    For each element in ``x1``, split the element at the first
+    occurrence of ``x2``, and return a 3-tuple containing the part
path: numpy/_core/code_generators/ufunc_docstrings.py
text: @user1:
I think it should be clear that the ufunc does not actually find the separator, so maybe
```
For each element in ``x``, split the element at the occurrence
of ``x2`` at location ``x3``, and return a 3-tuple ...
```

diff_hunk: @@ -5028,3 +5028,91 @@ def add_newdoc(place, name, doc):
     array(['001', '-01', '+01'], dtype='<U3')
 
     """)
+
+add_newdoc('numpy._core.umath', '_partition',
+    """
+    Partition each element in ``x1`` around ``x2``.
+
+    For each element in ``x1``, split the element at the first
+    occurrence of ``x2``, and return a 3-tuple containing the part
+    before the separator, the separator itself, and the part after
+    the separator. If the separator is not found, the first item of
+    the tuple will contain the whole string, and the second and third
+    ones will be the empty string.
+
+    Parameters
+    ----------
+    x1 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Input array
+    x2 : array-like, with ``StringDType``, ``bytes_``, or ``str_`` dtype
+        Separator to split each string element in ``x1``.
+    x3 : array-like, with any integer dtype
+        The indices of the separator
+
+    Returns
+    -------
+    out : 3-tuple:
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part before the separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          separator
+        - array with ``StringDType``, ``bytes_`` or ``str_`` dtype with the
+          part after the separator
+
+    See Also
+    --------
+    str.partition
+
+    Examples
+    --------
+    >>> x = np.array(["Numpy is nice!"])
+    >>> np.strings.partition(x, " ")
+    (array(['Numpy'], dtype='<U5'),
+     array([ True]),
path: numpy/_core/code_generators/ufunc_docstrings.py
text: @user1:
should be `array[' ']`, right?