Repository: numpy/numpy
Pull request: DEP: Deprecate `np.find_common_type`
The function uses the numeric scalar common dtype/promotion rules.
These are subtly different from the typical NumPy rules defined by
`np.result_type`.
Mainly, there is no good reason to have two subtly different rules
exposed and `find_common_type` is less reliable, slower, and not really
maintainable when it comes to NEP 50.

Closes #21703.

---

The function was used once within NumPy, and that is in the AxisConcatenator which implements `np.r_` and `np.c_`.  I replaced the occurrence there for `np.result_type` which mildly changes behavior in some odd cases like:
```
np.r_[np.arange(10, dtype=np.uint8), -1]  # int16 result rather than int64
np.r_[np.arange(10, dtype=np.int8), 255]  # int16, rather than int8
```
(cases where unsigned arrays are mixed with signed scalars (not 0-D arrays even), and cases which currently lead to overflows during casts.)

Closes gh-21703

Problem statement:
DEP: Deprecate `find_common_type` which is broken e.g. for datetimes
### Describe the issue:

When I call `np.find_common_type` with multiple identical `datetime64` types, I would expect to get the same type back, since all the inputs are identical. Instead, I always get `dtype('O')`.

### Reproduce the code example:

```python
import numpy as np
dt = np.dtype('datetime64[ns]')
dt1 = np.find_common_type([dt], []) 
# this works; both dtype('<M8')
assert dt1 == dt
dt2 = np.find_common_type([dt, dt], [])
# this fails; dt2 is dtype('O')
assert dt2 == dt
```


### Error message:

_No response_

### NumPy/Python version information:

1.21.2 3.9.9 | packaged by conda-forge | (main, Dec 20 2021, 02:41:37) 
[Clang 11.1.0 ]

Hints from issue discussion:
Hmmmpff... Thanks for the report!  I suspect that function has even more problems (It also seems plausible that there are duplicate issues open, but I did not check yet).

My preference would be to just deprecate it entirely.

If at all possible, I *strongly* encourage to always use `np.promote_types` or `np.result_type` (which also supports array inputs and some of that weirder "scalar" logic).

(And if not possible, please tell me, because that deprecation may just happen otherwise :)).
Thanks! `np.promote_types` does what I hoped.

Deprecate away! 🚀 
This might be a nice task for a sprint
Yeah, there is a small rabbit hole that we use it for `np.r_` (and maybe one other place).  I think that can be replaced there without breaking too much.  So it should be doable, but needs a bit of care.
This API states that scalars cannot upcast the dtype of an array. 
```
>>> np.find_common_type([np.int8], [np.int16])
dtype('int8')
```
But in fact:
```
>>> (np.array([1, 2, 3], dtype="int8") + np.int16(256)).dtype
dtype('int16')
```
I think it's yet another reason to deprecate.

Commit message: DEP,TYP: Remove `find_common_type` typing stubs and tests

Patch to review:
diff --git a/doc/release/upcoming_changes/22539.change.rst b/doc/release/upcoming_changes/22539.change.rst
new file mode 100644
index 000000000000..d4db966a0aa0
--- /dev/null
+++ b/doc/release/upcoming_changes/22539.change.rst
@@ -0,0 +1,19 @@
+``np.r_[]`` and ``np.c_[]`` with certain scalar values
+------------------------------------------------------
+In rare cases, using mainly ``np.r_`` with scalars can lead to different
+results.  The main potential changes are highlighted by the following::
+
+    >>> np.r_[np.arange(5, dtype=np.uint8), -1].dtype
+    int16  # rather than the default integer (int64 or int32)
+    >>> np.r_[np.arange(5, dtype=np.int8), 255]
+    array([  0,   1,   2,   3,   4, 255], dtype=int16)
+
+Where the second example returned::
+
+    array([ 0,  1,  2,  3,  4, -1], dtype=int8)
+
+The first one is due to a signed integer scalar with an unsigned integer
+array, while the second is due to ``255`` not fitting into ``int8`` and
+NumPy currently inspecting values to make this work.
+(Note that the second example is expected to change in the future due to
+:ref:`NEP 50 <NEP50>`)
diff --git a/doc/release/upcoming_changes/22539.deprecation.rst b/doc/release/upcoming_changes/22539.deprecation.rst
new file mode 100644
index 000000000000..2c4dabf8f8dc
--- /dev/null
+++ b/doc/release/upcoming_changes/22539.deprecation.rst
@@ -0,0 +1,24 @@
+``np.find_common_type`` is deprecated
+-------------------------------------
+`numpy.find_common_type` is now deprecated and its use should be replaced
+with either `numpy.result_type` or `numpy.promote_types`.
+Most users leave the second ``scalar_types`` argument to ``find_common_type``
+as ``[]`` in which case ``np.result_type`` and ``np.promote_types`` are both
+faster and more robust.
+When not using ``scalar_types`` the only difference is that the replacement
+intentionally converts non-native byte-order to native byte order.
+
+When the second argument is used things are more complicated.
+In most cases, using ``np.result_type`` and passing the Python values
+``0``, ``0.0``, or ``0j`` has the same result as using ``int``, ``float``,
+or ``complex`` in `scalar_types`.
+
+When ``scalar_types`` is constructed, ``npy.result_type`` is the
+correct replacement and it may be passed scalar values like ``np.float32(0.0)``.
+Passing values other than 0, may lead to value-inspecting behavior
+(which ``np.find_common_type`` never used and NEP 50 may change in the future).
+The main possible change in behavior in this case, is when the array types
+are signed integers and scalar types are unsigned.
+
+If you are unsure about how to replace an a use of ``scalar_types``, please
+do not hesitate to open a NumPy issue.
diff --git a/numpy/core/numerictypes.py b/numpy/core/numerictypes.py
index 3d1cb6fd19c0..8b210a1a1b25 100644
--- a/numpy/core/numerictypes.py
+++ b/numpy/core/numerictypes.py
@@ -80,6 +80,7 @@
 
 """
 import numbers
+import warnings
 
 from numpy.core.multiarray import (
         ndarray, array, dtype, datetime_data, datetime_as_string,
@@ -599,6 +600,16 @@ def find_common_type(array_types, scalar_types):
     """
     Determine common type following standard coercion rules.
 
+    .. deprecated:: NumPy 1.24
+
+        This function is deprecated, use `numpy.promote_types` or
+        `numpy.result_type` instead.  To achieve semantics for the
+        `scalar_types` argument, use `numpy.result_type` and pass the Python
+        values `0`, `0.0`, or `0j`.
+        This will give the same results in almost all cases.
+        More information and rare exception can be found in the
+        `NumPy 1.24 release notes <https://numpy.org/devdocs/release/1.24.0-notes.html>`_.
+
     Parameters
     ----------
     array_types : sequence
@@ -646,6 +657,14 @@ def find_common_type(array_types, scalar_types):
     dtype('complex128')
 
     """
+    # Deprecated 2022-11-07, NumPy 1.24
+    warnings.warn(
+            "np.find_common_type is deprecated.  Please use `np.result_type` "
+            "or `np.promote_types`.\n"
+            "See https://numpy.org/devdocs/release/1.24.0-notes.html and the "
+            "docs for more information.  (Deprecated NumPy 1.24)",
+            DeprecationWarning, stacklevel=2)
+
     array_types = [dtype(x) for x in array_types]
     scalar_types = [dtype(x) for x in scalar_types]
 
diff --git a/numpy/core/numerictypes.pyi b/numpy/core/numerictypes.pyi
index d10e4822ad09..d05861b2eec6 100644
--- a/numpy/core/numerictypes.pyi
+++ b/numpy/core/numerictypes.pyi
@@ -118,11 +118,6 @@ def issubdtype(arg1: DTypeLike, arg2: DTypeLike) -> bool: ...
 
 def sctype2char(sctype: DTypeLike) -> str: ...
 
-def find_common_type(
-    array_types: Iterable[DTypeLike],
-    scalar_types: Iterable[DTypeLike],
-) -> dtype[Any]: ...
-
 cast: _typedict[_CastFunc]
 nbytes: _typedict[int]
 typecodes: _TypeCodes
diff --git a/numpy/core/tests/test_numerictypes.py b/numpy/core/tests/test_numerictypes.py
index ffe8716e71f2..6aedbf569e75 100644
--- a/numpy/core/tests/test_numerictypes.py
+++ b/numpy/core/tests/test_numerictypes.py
@@ -339,23 +339,28 @@ def test_assign(self):
 
 class TestCommonType:
     def test_scalar_loses1(self):
-        res = np.find_common_type(['f4', 'f4', 'i2'], ['f8'])
+        with pytest.warns(DeprecationWarning, match="np.find_common_type"):
+            res = np.find_common_type(['f4', 'f4', 'i2'], ['f8'])
         assert_(res == 'f4')
 
     def test_scalar_loses2(self):
-        res = np.find_common_type(['f4', 'f4'], ['i8'])
+        with pytest.warns(DeprecationWarning, match="np.find_common_type"):
+            res = np.find_common_type(['f4', 'f4'], ['i8'])
         assert_(res == 'f4')
 
     def test_scalar_wins(self):
-        res = np.find_common_type(['f4', 'f4', 'i2'], ['c8'])
+        with pytest.warns(DeprecationWarning, match="np.find_common_type"):
+            res = np.find_common_type(['f4', 'f4', 'i2'], ['c8'])
         assert_(res == 'c8')
 
     def test_scalar_wins2(self):
-        res = np.find_common_type(['u4', 'i4', 'i4'], ['f4'])
+        with pytest.warns(DeprecationWarning, match="np.find_common_type"):
+            res = np.find_common_type(['u4', 'i4', 'i4'], ['f4'])
         assert_(res == 'f8')
 
     def test_scalar_wins3(self):  # doesn't go up to 'f16' on purpose
-        res = np.find_common_type(['u8', 'i8', 'i8'], ['f8'])
+        with pytest.warns(DeprecationWarning, match="np.find_common_type"):
+            res = np.find_common_type(['u8', 'i8', 'i8'], ['f8'])
         assert_(res == 'f8')
 
 class TestMultipleFields:
diff --git a/numpy/core/tests/test_regression.py b/numpy/core/tests/test_regression.py
index 427e868e8d15..c3b9dab696e7 100644
--- a/numpy/core/tests/test_regression.py
+++ b/numpy/core/tests/test_regression.py
@@ -1673,7 +1673,9 @@ def test_nonzero_byteswap(self):
 
     def test_find_common_type_boolean(self):
         # Ticket #1695
-        assert_(np.find_common_type([], ['?', '?']) == '?')
+        with pytest.warns(DeprecationWarning, match="np.find_common_type"):
+            res = np.find_common_type([], ['?', '?'])
+        assert res == '?'
 
     def test_empty_mul(self):
         a = np.array([1.])
diff --git a/numpy/lib/index_tricks.py b/numpy/lib/index_tricks.py
index 4f414925d158..4c407d643a41 100644
--- a/numpy/lib/index_tricks.py
+++ b/numpy/lib/index_tricks.py
@@ -7,7 +7,7 @@
 from numpy.core.numeric import (
     asarray, ScalarType, array, alltrue, cumprod, arange, ndim
 )
-from numpy.core.numerictypes import find_common_type, issubdtype
+from numpy.core.numerictypes import issubdtype
 
 import numpy.matrixlib as matrixlib
 from .function_base import diff
@@ -339,9 +339,8 @@ def __getitem__(self, key):
         axis = self.axis
 
         objs = []
-        scalars = []
-        arraytypes = []
-        scalartypes = []
+        # dtypes or scalars for weak scalar handling in result_type
+        result_type_objs = []
 
         for k, item in enumerate(key):
             scalar = False
@@ -387,10 +386,8 @@ def __getitem__(self, key):
                 except (ValueError, TypeError) as e:
                     raise ValueError("unknown special directive") from e
             elif type(item) in ScalarType:
-                newobj = array(item, ndmin=ndmin)
-                scalars.append(len(objs))
                 scalar = True
-                scalartypes.append(newobj.dtype)
+                newobj = array(item, ndmin=ndmin)
             else:
                 item_ndim = ndim(item)
                 newobj = array(item, copy=False, subok=True, ndmin=ndmin)
@@ -402,15 +399,19 @@ def __getitem__(self, key):
                     defaxes = list(range(ndmin))
                     axes = defaxes[:k1] + defaxes[k2:] + defaxes[k1:k2]
                     newobj = newobj.transpose(axes)
+
             objs.append(newobj)
-            if not scalar and isinstance(newobj, _nx.ndarray):
-                arraytypes.append(newobj.dtype)
-
-        # Ensure that scalars won't up-cast unless warranted
-        final_dtype = find_common_type(arraytypes, scalartypes)
-        if final_dtype is not None:
-            for k in scalars:
-                objs[k] = objs[k].astype(final_dtype)
+            if scalar:
+                result_type_objs.append(item)
+            else:
+                result_type_objs.append(newobj.dtype)
+
+        # Ensure that scalars won't up-cast unless warranted, for 0, drops
+        # through to error in concatenate.
+        if len(result_type_objs) != 0:
+            final_dtype = _nx.result_type(*result_type_objs)
+            # concatenate could do cast, but that can be overriden:
+            objs = [obj.astype(final_dtype, copy=False) for obj in objs]
 
         res = self.concatenate(tuple(objs), axis=axis)
 
diff --git a/numpy/ma/tests/test_core.py b/numpy/ma/tests/test_core.py
index f32038a0118d..6196dcfab2f0 100644
--- a/numpy/ma/tests/test_core.py
+++ b/numpy/ma/tests/test_core.py
@@ -4461,7 +4461,7 @@ def test_where_type(self):
         x = np.arange(4, dtype=np.int32)
         y = np.arange(4, dtype=np.float32) * 2.2
         test = where(x > 1.5, y, x).dtype
-        control = np.find_common_type([np.int32, np.float32], [])
+        control = np.result_type(np.int32, np.float32)
         assert_equal(test, control)
 
     def test_where_broadcast(self):
diff --git a/numpy/typing/tests/data/fail/numerictypes.pyi b/numpy/typing/tests/data/fail/numerictypes.pyi
index a5c2814ef119..ce5662d5e66a 100644
--- a/numpy/typing/tests/data/fail/numerictypes.pyi
+++ b/numpy/typing/tests/data/fail/numerictypes.pyi
@@ -9,5 +9,3 @@ np.maximum_sctype(1)  # E: No overload variant
 np.issubsctype(1, np.int64)  # E: incompatible type
 
 np.issubdtype(1, np.int64)  # E: incompatible type
-
-np.find_common_type(np.int64, np.int64)  # E: incompatible type
diff --git a/numpy/typing/tests/data/pass/numerictypes.py b/numpy/typing/tests/data/pass/numerictypes.py
index 7f1dd0945438..8d6a390748bb 100644
--- a/numpy/typing/tests/data/pass/numerictypes.py
+++ b/numpy/typing/tests/data/pass/numerictypes.py
@@ -23,11 +23,6 @@
 np.sctype2char("S1")
 np.sctype2char(list)
 
-np.find_common_type([], [np.int64, np.float32, complex])
-np.find_common_type((), (np.int64, np.float32, complex))
-np.find_common_type([np.int64, np.float32], [])
-np.find_common_type([np.float32], [np.int64, np.float64])
-
 np.cast[int]
 np.cast["i8"]
 np.cast[np.int64]
diff --git a/numpy/typing/tests/data/reveal/numerictypes.pyi b/numpy/typing/tests/data/reveal/numerictypes.pyi
index e1857557d90a..d4399e2b121a 100644
--- a/numpy/typing/tests/data/reveal/numerictypes.pyi
+++ b/numpy/typing/tests/data/reveal/numerictypes.pyi
@@ -21,8 +21,6 @@ reveal_type(np.issubclass_(1, 1))  # E: Literal[False]
 reveal_type(np.sctype2char("S8"))  # E: str
 reveal_type(np.sctype2char(list))  # E: str
 
-reveal_type(np.find_common_type([np.int64], [np.int64]))  # E: dtype[Any]
-
 reveal_type(np.cast[int])  # E: _CastFunc
 reveal_type(np.cast["i8"])  # E: _CastFunc
 reveal_type(np.cast[np.int64])  # E: _CastFunc


Code review comments:
diff_hunk: @@ -0,0 +1,24 @@
+``np.find_common_type`` is deprecated
+-------------------------------------
+`numpy.find_common_type` is now deprecated and its use should be replaced
+with either `numpy.result_type` or `numpy.promote_types`.
+Most users leave the second ``scalar_types`` argument to ``find_common_type``
+as ``[]`` in which case ``np.result_type`` and ``np.promote_types`` are both
+faster and more robust.
+When not using ``scalar_types`` the only difference is that the replacement
+intentionally converts non-native byte-order to native byte order.
+
+When the second argument is used things are more complicated.
+In most cases, using ``np.result_type`` and passing the Python values
+``0``, ``0.0``, or ``0j`` has the same result as using ``int``, ``float``,
+or ``complex`` in `scalar_types`.
+
+When ``scalar_types`` is constructed, ``npy.result_type`` is the
path: doc/release/upcoming_changes/22539.deprecation.rst
text: @user1:
```suggestion
When ``scalar_types`` is constructed, ``np.result_type`` is the
```