Repository: numpy/numpy
Pull request: MAINT: Improve error message when common type not found.
Fixes gh-18430

Problem statement:
ENH: A more helpful error message, when `np.select` types don't match type of default kwarg 
The numpy.select function understandably raises an error when the type of the values in the choicelist doesn't match that of the default argument.

The following code:

```python
conds1 = [True, False, False]
conds2 = [False, True, False]
conds3 = [False, False, True]

dates1 = [np.datetime64('2020-01-02'), np.datetime64('2020-02-03'), np.datetime64('2020-03-04')]
dates2 = [d + np.timedelta64(52, 'W') for d in dates1]
dates3 = [d + np.timedelta64(52, 'W') for d in dates2]

np.select([conds1, conds2, conds3], [dates1, dates2, dates3]) # default=0 by default
```

Will raise:

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-38-59398fcf15ab> in <module>
----> 1 np.select([conds1, conds2, conds3], [dates1, dates2, dates3])

<__array_function__ internals> in select(*args, **kwargs)

~\Miniconda3\lib\site-packages\numpy\lib\function_base.py in select(condlist, choicelist, default)
    674     # need to get the result type before broadcasting for correct scalar
    675     # behaviour
--> 676     dtype = np.result_type(*choicelist)
    677 
    678     # Convert conditions to arrays and broadcast conditions and choices

<__array_function__ internals> in result_type(*args, **kwargs)

TypeError: invalid type promotion

```

The above code can be fixed by 

```python
np.select([conds1, conds2, conds3], [dates1, dates2, dates3], default=np.datetime64('NaT'))
```

outputs:

```
array(['2020-01-02', '2021-02-01', '2022-03-02'], dtype='datetime64[D]')
```

as expected

The error message in the first example, is quite obscure, especially to someone who is not aware of the default argument in the first place, since it is often omitted. It would be better if the error message was more descriptive, and hinted at the fact that the TypeError is happening due to a mismatch between the type of default, and the type of the values in the choicelists. 

Possible message:

```
TypeError: type mismatch between default parameter and type of choicelists: 'int' != 'numpy.datetime64'
```

Hints from issue discussion:
For what its worth, the message is:
```
TypeError: The DTypes <class 'numpy.dtype[uint8]'> and <class 'numpy.dtype[datetime64]'> do not have a common DType. For example they cannot be stored in a single array unless the dtype is `object`.
```
right now. Although, I am starting to wonder if we should print the names shorter (also but not only the `class` part), and backport to 1.20.x... That probably is a bit confusing :(.
@seberg Thanks for pointing that out, I was using an older version. The current message is more helpful. It could still be a little confusing for someone who verifies that their choicelist types are consistent and yet the error is still being raised, as a default is not mentioned.

On the other hand, the documentation does say that the default is added to the output (implying the types need to match), perhaps that gives enough clarity together with the newer message.
Ah, the default is problematic with datetimes. I guess for datetimes creating a default is not quite trivial, but maybe there is actually an opportunity for a `select` (or at least more) specific error message here.

Commit message: ENH: A more helpful error message, when  types don't match type of default kwarg

Patch to review:
diff --git a/numpy/lib/function_base.py b/numpy/lib/function_base.py
index c6db42ce4905..ddc919e4f5dc 100644
--- a/numpy/lib/function_base.py
+++ b/numpy/lib/function_base.py
@@ -671,11 +671,23 @@ def select(condlist, choicelist, default=0):
         raise ValueError("select with an empty condition list is not possible")
 
     choicelist = [np.asarray(choice) for choice in choicelist]
-    choicelist.append(np.asarray(default))
+
+    try:
+        intermediate_dtype = np.result_type(*choicelist)
+    except TypeError as e:
+        raise TypeError('Choicelist elements do not have a common dtype: {}'
+                        .format(e))
+    default_array = np.asarray(default)
+    choicelist.append(default_array)
 
     # need to get the result type before broadcasting for correct scalar
     # behaviour
-    dtype = np.result_type(*choicelist)
+    try:
+        dtype = np.result_type(intermediate_dtype, default_array)
+    except TypeError as e:
+        raise TypeError(
+                'Choicelists and default do not have a common dtype: {}'
+                .format(e))
 
     # Convert conditions to arrays and broadcast conditions and choices
     # as the shape is needed for the result. Doing it separately optimizes


Code review comments:
diff_hunk: @@ -671,11 +671,23 @@ def select(condlist, choicelist, default=0):
         raise ValueError("select with an empty condition list is not possible")
 
     choicelist = [np.asarray(choice) for choice in choicelist]
-    choicelist.append(np.asarray(default))
+
+    try:
+        intermediate_dtype = np.result_type(*choicelist)
+    except TypeError as e:
+        raise TypeError('Choicelist elements do not have a common dtype: {}'
+                        .format(e))
path: numpy/lib/function_base.py
text: @user1:
can you use exception chaining here and further down? i.e. `raise TypeError(...) from e`

we get a lot of PRs to add exception chaining (`from e` or `from None`) so we may as well do that whenever we add new code.

@user2:
Right, it should have a `from None` if we include the old message in the new error.  Using an `f` string is a bit nicer, i.e. `f"...: {e}"`.

We don't do this type of replacement a lot, but it does make things clearer, so happy to put this in.