Repository: matplotlib/matplotlib

Issue:
[Bug]: Setting exactly 2 colors with tuple in `plot` method gives confusing error
### Bug summary

If one attempts to set the `color` parameter to a tuple of length 2, a nonsense error message is returned.

### Code for reproduction

```Python
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
y = np.array([np.sin(x), np.cos(x)]).T

plt.plot(x, y, label=("sin(x)", "cos(x)")) # works
plt.plot(x, y, label=("sin(x)", "cos(x)"), color=("red", "blue")) # causes strange error
plt.legend()
plt.show()
```


### Actual outcome

Traceback (most recent call last):
  File "/Users/miles/Desktop/thing.py", line 8, in <module>
    plt.plot(x, y, label=("sin(x)", "cos(x)"), color=("red", "blue")) # causes error
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/pyplot.py", line 3708, in plot
    return gca().plot(
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/axes/_axes.py", line 1779, in plot
    lines = [*self._get_lines(self, *args, data=data, **kwargs)]
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/axes/_base.py", line 296, in __call__
    yield from self._plot_args(
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/axes/_base.py", line 534, in _plot_args
    return [l[0] for l in result]
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/axes/_base.py", line 534, in <listcomp>
    return [l[0] for l in result]
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/axes/_base.py", line 527, in <genexpr>
    result = (make_artist(axes, x[:, j % ncx], y[:, j % ncy], kw,
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/axes/_base.py", line 335, in _makeline
    seg = mlines.Line2D(x, y, **kw)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/lines.py", line 376, in __init__
    self.set_color(color)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/lines.py", line 1066, in set_color
    mcolors._check_color_like(color=color)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/colors.py", line 245, in _check_color_like
    if not is_color_like(v):
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/colors.py", line 227, in is_color_like
    to_rgba(c)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/colors.py", line 309, in to_rgba
    rgba = _to_rgba_no_colorcycle(c, alpha)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/colors.py", line 334, in _to_rgba_no_colorcycle
    if alpha is not None and not 0 <= alpha <= 1:
TypeError: '<=' not supported between instances of 'int' and 'str'

### Expected outcome

Either: (1) works as expected as passing a tuple to `label` does, or, if this won't be supported, (2) a more sensical error message is returned, like if `["red", "blue"]` were passed.

### Additional information

This difficult-to-interpret error seems to be caused by the `if` statement of line 328 in `_to_rgba_no_colorcycle` in `colors.py`, which says `if isinstance(c, tuple) and len(c) == 2`. If I comment out this `if` statement, then the error message gives the normal `ValueError: ('red', 'blue') is not a valid value for color...` message. Of course, it would be great if multiple colors were supported here, but I understand if there is a bigger reason why it is not supported.

### Operating system

macOS 14.4.1

### Matplotlib Version

3.9.0

### Matplotlib Backend

macosx

### Python version

3.9.6

### Jupyter version

_No response_

### Installation

pip


Discussion:
This is a result of the `(color, alpha)` version of specifying colors that was introduced in [3.8.0](https://matplotlib.org/stable/users/prev_whats_new/whats_new_3.8.0.html#add-a-new-valid-color-format-matplotlib-color-alpha)

That is why it is specifically tuples (not lists) and specifically length 2...

My gut reaction is that the new color specifier is useful enough and the workaround (casting to list) is easy enough that I'm inclined towards keeping current behavior...

On the other hand, it may be reasonable to narrow the condition further to "is the second entry in a length 2 tuple a number"... and if it is not, treat it as a standard sequence of 2 colors instead...

As long as numbers are not valid color specifiers, this would differentiate the cases. I believe that is the case (though _string_ floating point values _are_ valid color specifiers for shades of gray...). Because if a number were a valid color specifier, then this would be an ambiguous case.
Just catching the `TypeError` within `is_color_like` makes the error consistent with the list case

```patch
diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py
index c4e5987fdf..f6e78dc3e4 100644
--- a/lib/matplotlib/colors.py
+++ b/lib/matplotlib/colors.py
@@ -225,7 +225,7 @@ def is_color_like(c):
         return True
     try:
         to_rgba(c)
-    except ValueError:
+    except (ValueError, TypeError):
         return False
     else:
         return True

```
```
ValueError: ('red', 'blue') is not a valid value for color: supported inputs are (r, g, b) and (r, g, b, a) 0-1 float tuples; '#rrggbb', '#rrggbbaa', '#rgb', '#rgba' strings; named color strings; string reprs of 0-1 floats for grayscale values; 'C0', 'C1', ... strings for colors of the color cycle; and pairs combining one of the above with an alpha value

```
Note there is no issue with functions that do take a color sequence
```python
plt.scatter([1, 2], [4, 5], c=("red", "blue"))
```
![image](https://github.com/matplotlib/matplotlib/assets/10599679/f2504f6e-3159-4619-b090-f6ff81d81053)

> Note there is no issue with functions that do take a color sequence

Do we want to allow this function to take a color sequence? It would be in line w/ the other PRs that are vectorizing inputs.
It does seem odd that by default you get 2 colors but you cannot specify 2 colors.
The `_is_color_like` change proposed by @rcomer [above](https://github.com/matplotlib/matplotlib/issues/28434#issuecomment-2183340319) should be applied no matter what.

I'm a bit hesitant on supporting sequences for color. If we do that, we'd have to expand to all parameters consistently. Note in particular that some parameters use sequences as single-value represenations already, e.g. colors and [linestyles](https://matplotlib.org/stable/gallery/lines_bars_and_markers/linestyles.html#linestyles), so detection of sequence-of-parameters is non-trivial. It's not completely unreasonable to support seqnce-of-parameters, but adds significant complexity to an already very complex function. If somebody implements this, I'd like to see an as-clean-as possible implementation.

When adding label sequences, we've explicitly done that as an exception without the promise to support sequences on all parameters. The argument is that all other parameters can be addressed through the property cycle, i.e. can be configured up-front to a multiple-data plot. That's not possible for labels. See https://github.com/matplotlib/matplotlib/pull/16178#issuecomment-750528043.
I think that in general the idea of two d conveniences for one d plotting functions is more trouble than it is worth and leads to an inconsistent API. In fact, I'd argue that allowing lists of labels was a bad idea to start with.  If people need to style individual columns of a data set differently, and they don't want to deal with our cycler, they can use a trivial for-loop.  (I see I am repeating myself from #16178)

Of course I do agree that the error message is terrible here.  



Patch:
diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py
index c4e5987fdf92..177557b371a6 100644
--- a/lib/matplotlib/colors.py
+++ b/lib/matplotlib/colors.py
@@ -225,7 +225,7 @@ def is_color_like(c):
         return True
     try:
         to_rgba(c)
-    except ValueError:
+    except (TypeError, ValueError):
         return False
     else:
         return True
@@ -296,6 +296,11 @@ def to_rgba(c, alpha=None):
         Tuple of floats ``(r, g, b, a)``, where each channel (red, green, blue,
         alpha) can assume values between 0 and 1.
     """
+    if isinstance(c, tuple) and len(c) == 2:
+        if alpha is None:
+            c, alpha = c
+        else:
+            c = c[0]
     # Special-case nth color syntax because it should not be cached.
     if _is_nth_color(c):
         prop_cycler = mpl.rcParams['axes.prop_cycle']
@@ -325,11 +330,6 @@ def _to_rgba_no_colorcycle(c, alpha=None):
     *alpha* is ignored for the color value ``"none"`` (case-insensitive),
     which always maps to ``(0, 0, 0, 0)``.
     """
-    if isinstance(c, tuple) and len(c) == 2:
-        if alpha is None:
-            c, alpha = c
-        else:
-            c = c[0]
     if alpha is not None and not 0 <= alpha <= 1:
         raise ValueError("'alpha' must be between 0 and 1, inclusive")
     orig_c = c


Test patch:
diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py
index c8b44b2dea14..d99dd91e9cf5 100644
--- a/lib/matplotlib/tests/test_colors.py
+++ b/lib/matplotlib/tests/test_colors.py
@@ -19,7 +19,7 @@
 import matplotlib.scale as mscale
 from matplotlib.rcsetup import cycler
 from matplotlib.testing.decorators import image_comparison, check_figures_equal
-from matplotlib.colors import to_rgba_array
+from matplotlib.colors import is_color_like, to_rgba_array
 
 
 @pytest.mark.parametrize('N, result', [
@@ -1702,3 +1702,16 @@ def test_to_rgba_array_none_color_with_alpha_param():
     assert_array_equal(
         to_rgba_array(c, alpha), [[0., 0., 1., 1.], [0., 0., 0., 0.]]
     )
+
+
+@pytest.mark.parametrize('input, expected',
+                         [('red', True),
+                          (('red', 0.5), True),
+                          (('red', 2), False),
+                          (['red', 0.5], False),
+                          (('red', 'blue'), False),
+                          (['red', 'blue'], False),
+                          ('C3', True),
+                          (('C3', 0.5), True)])
+def test_is_color_like(input, expected):
+    assert is_color_like(input) is expected