Repository: pydata/xarray

Issue:
# Unexpected keyword error in `dataarray_plot.py`

### What happened?

When I tried to plot 2D flow field (Time * Nx * Ny np array), I got "**AxesImage.set() got an unexpected keyword argument 'msg'**", which I found came from `dataarray_plot._plot2d.newplotfunc`. Here is the detail:

---

OS: Ubuntu 24.04 LTS, WSL2
python version: 3.12.8
numpy version: 2.2.6
xarray version: 2025.3.0
matplotlib version: 3.10.0 (newest on Jan. 26 2026)

---

My code snippet:
```
# load into xarray for visualization and analysis
ds = xarray.Dataset(
    {
        'u': (('time', 'x', 'y'), trajectory[0].data), # (10, 128, 128) array
        'v': (('time', 'x', 'y'), trajectory[1].data), # (10, 128, 128) array
    },
    coords={
        'x': grid.axes()[0], # (128,) array
        'y': grid.axes()[1], # (128,) array
        'time': dt * inner_steps * np.arange(1, outer_steps + 1) # (10,) array
    }
)

ds["vorticity"] = ds["v"].differentiate("x") - ds["u"].differentiate("y") # (10, 128, 128) array

# Plot the x-velocity
ds.pipe(lambda ds: ds.u).plot.imshow('x', 'y', col='time', cmap=seaborn.cm.rocket, robust=True, col_wrap=4, aspect=2)
```

The error message is attached below.


### What did you expect to happen?

_No response_

### Minimal Complete Verifiable Example

```Python
# /// script
# requires-python = ">=3.11"
# dependencies = [
#   "xarray[complete]@git+https://github.com/pydata/xarray.git@main",
# ]
# ///
#
# This script automatically imports the development branch of xarray to check for issues.
# Please delete this header if you have _not_ tested this script with `uv run`!

import xarray as xr
xr.show_versions()
# your reproducer code ...
# load into xarray for visualization and analysis
ds = xarray.Dataset(
    {
        'u': (('time', 'x', 'y'), np.random.randn(10, 128, 128)), # (10, 128, 128) array
        'v': (('time', 'x', 'y'), np.random.randn(10, 128, 128)), # (10, 128, 128) array
    },
    coords={
        'x': np.linspace(0, 2*np.pi, 128), # (128,) array
        'y': np.linspace(0, 2*np.pi, 128), # (128,) array
        'time': np.linspace(0, 1, 10) # (10,) array
    }
)

ds.u.plot.imshow('x', 'y', col='time')
```

### Steps to reproduce

Just run the minimal complete verifiable example.

### MVCE confirmation

- [x] Minimal example — the example is as focused as reasonably possible to demonstrate the underlying issue in xarray.
- [x] Complete example — the example is self-contained, including all data and the text of any traceback.
- [x] Verifiable example — the example copy & pastes into an IPython prompt or [Binder notebook](https://mybinder.org/v2/gh/pydata/xarray/main?urlpath=lab/tree/doc/examples/blank_template.ipynb), returning the result.
- [x] New issue — a search of GitHub Issues suggests this is not a duplicate.
- [ ] Recent environment — the issue occurs with the latest version of xarray and its dependencies.

### Relevant log output

```Python
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[20], line 14
      1 # load into xarray for visualization and analysis
      2 ds = xarray.Dataset(
      3     {
      4         'u': (('time', 'x', 'y'), np.random.randn(10, 128, 128)), # (10, 128, 128) array
   (...)     11     }
     12 )
---> 14 ds.u.plot.imshow('x', 'y', col='time')

File ***/site-packages/xarray/plot/accessor.py:421, in DataArrayPlotAccessor.imshow(self, *args, **kwargs)
    419 @functools.wraps(dataarray_plot.imshow, assigned=("__doc__",))
    420 def imshow(self, *args, **kwargs) -> AxesImage | FacetGrid[DataArray]:
--> 421     return dataarray_plot.imshow(self._da, *args, **kwargs)

File ***/site-packages/xarray/plot/dataarray_plot.py:1507, in _plot2d.<locals>.newplotfunc(***failed resolving arguments***)
   1505     # Need the decorated plotting function
   1506     allargs["plotfunc"] = globals()[plotfunc.__name__]
-> 1507     return _easy_facetgrid(darray, kind="dataarray", **allargs)
   1509 if darray.ndim == 0 or darray.size == 0:
   1510     # TypeError to be consistent with pandas
   1511     raise TypeError("No numeric data to plot.")

File ***/site-packages/xarray/plot/facetgrid.py:1072, in _easy_facetgrid(data, plotfunc, kind, x, y, row, col, col_wrap, sharex, sharey, aspect, size, subplot_kws, ax, figsize, **kwargs)
   1069     return g.map_dataarray_line(plotfunc, x, y, **kwargs)
   1071 if kind == "dataarray":
-> 1072     return g.map_dataarray(plotfunc, x, y, **kwargs)
   1074 if kind == "plot1d":
   1075     return g.map_plot1d(plotfunc, x, y, **kwargs)

File ***/site-packages/xarray/plot/facetgrid.py:373, in FacetGrid.map_dataarray(self, func, x, y, **kwargs)
    371     if d is not None:
    372         subset = self.data.loc[d]
--> 373         mappable = func(
    374             subset, x=x, y=y, ax=ax, **func_kwargs, _is_facetgrid=True
    375         )
    376         self._mappables.append(mappable)
    378 self._finalize_grid(x, y)

File ***/site-packages/xarray/plot/dataarray_plot.py:1607, in _plot2d.<locals>.newplotfunc(***failed resolving arguments***)
   1603     raise ValueError("plt.imshow's `aspect` kwarg is not available in xarray")
   1605 ax = get_axis(figsize, size, aspect, ax, **subplot_kws)
-> 1607 primitive = plotfunc(
   1608     xplt,
   1609     yplt,
   1610     zval,
   1611     ax=ax,
   1612     cmap=cmap_params["cmap"],
   1613     vmin=cmap_params["vmin"],
   1614     vmax=cmap_params["vmax"],
   1615     norm=cmap_params["norm"],
   1616     **kwargs,
   1617 )
   1619 # Label the plot with metadata
   1620 if add_labels:

File ***/site-packages/xarray/plot/dataarray_plot.py:1866, in imshow(x, y, z, ax, **kwargs)
   1863         z = z.copy()
   1864     z[np.any(z.mask, axis=-1), -1] = 0
-> 1866 primitive = ax.imshow(z, **defaults)
   1868 # If x or y are strings the ticklabels have been replaced with
   1869 # integer indices. Replace them back to strings:
   1870 for axis, v in [("x", x), ("y", y)]:

File ***/site-packages/matplotlib/__init__.py:1521, in _preprocess_data.<locals>.inner(ax, data, *args, **kwargs)
   1518 @functools.wraps(func)
   1519 def inner(ax, *args, data=None, **kwargs):
   1520     if data is None:
-> 1521         return func(
   1522             ax,
   1523             *map(cbook.sanitize_sequence, args),
   1524             **{k: cbook.sanitize_sequence(v) for k, v in kwargs.items()})
   1526     bound = new_sig.bind(ax, *args, **kwargs)
   1527     auto_label = (bound.arguments.get(label_namer)
   1528                   or bound.kwargs.get(label_namer))

File ***/site-packages/matplotlib/axes/_axes.py:5931, in Axes.imshow(self, X, cmap, norm, aspect, interpolation, alpha, vmin, vmax, colorizer, origin, extent, interpolation_stage, filternorm, filterrad, resample, url, **kwargs)
   5713 @_preprocess_data()
   5714 @_docstring.interpd
   5715 def imshow(self, X, cmap=None, norm=None, *, aspect=None,
   (...)   5718            interpolation_stage=None, filternorm=True, filterrad=4.0,
   5719            resample=None, url=None, **kwargs):
   5720     """
   5721     Display data as an image, i.e., on a 2D regular raster.
   5722 
   (...)   5929     (unassociated) alpha representation.
   5930     """
-> 5931     im = mimage.AxesImage(self, cmap=cmap, norm=norm, colorizer=colorizer,
   5932                           interpolation=interpolation, origin=origin,
   5933                           extent=extent, filternorm=filternorm,
   5934                           filterrad=filterrad, resample=resample,
   5935                           interpolation_stage=interpolation_stage,
   5936                           **kwargs)
   5938     if aspect is None and not (
   5939             im.is_transform_set()
   5940             and not im.get_transform().contains_branch(self.transData)):
   5941         aspect = mpl.rcParams['image.aspect']

File ***/site-packages/matplotlib/image.py:874, in AxesImage.__init__(self, ax, cmap, norm, colorizer, interpolation, origin, extent, filternorm, filterrad, resample, interpolation_stage, **kwargs)
    857 def __init__(self, ax,
    858              *,
    859              cmap=None,
   (...)    869              **kwargs
    870              ):
    872     self._extent = extent
--> 874     super().__init__(
    875         ax,
    876         cmap=cmap,
    877         norm=norm,
    878         colorizer=colorizer,
    879         interpolation=interpolation,
    880         origin=origin,
    881         filternorm=filternorm,
    882         filterrad=filterrad,
    883         resample=resample,
    884         interpolation_stage=interpolation_stage,
    885         **kwargs
    886     )

File ***/site-packages/matplotlib/image.py:277, in _ImageBase.__init__(self, ax, cmap, norm, colorizer, interpolation, origin, filternorm, filterrad, resample, interpolation_stage, **kwargs)
    273 self.axes = ax
    275 self._imcache = None
--> 277 self._internal_update(kwargs)

File ***/site-packages/matplotlib/artist.py:1233, in Artist._internal_update(self, kwargs)
   1226 def _internal_update(self, kwargs):
   1227     """
   1228     Update artist properties without prenormalizing them, but generating
   1229     errors as if calling `set`.
   1230 
   1231     The lack of prenormalization is to maintain backcompatibility.
   1232     """
-> 1233     return self._update_props(
   1234         kwargs, "{cls.__name__}.set() got an unexpected keyword argument "
   1235         "{prop_name!r}")

File ***/site-packages/matplotlib/artist.py:1206, in Artist._update_props(self, props, errfmt)
   1204             func = getattr(self, f"set_{k}", None)
   1205             if not callable(func):
-> 1206                 raise AttributeError(
   1207                     errfmt.format(cls=type(self), prop_name=k),
   1208                     name=k)
   1209             ret.append(func(v))
   1210 if ret:

AttributeError: AxesImage.set() got an unexpected keyword argument 'msg'
```

### Anything else we need to know?

In `dataarray_plot._plot2d.newplotfunc`, this line of code `allargs = locals().copy()` added the local variable into `allargs`, which contains key `msg` and triggers the error from matplotlib.

Even though I am not using the newest version, I find this line of code still exists in the latest release.

### Environment

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.12.8 | packaged by Anaconda, Inc. | (main, Dec 11 2024, 16:31:09) [GCC 11.2.0]
python-bits: 64
OS: Linux
OS-release: 6.6.87.2-microsoft-standard-WSL2
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: C.UTF-8
LOCALE: ('C', 'UTF-8')
libhdf5: 1.14.6
libnetcdf: 4.9.3

xarray: 2025.3.0
pandas: 2.2.3
numpy: 2.2.6
scipy: 1.15.3
netCDF4: 1.7.4
pydap: None
h5netcdf: None
h5py: None
zarr: None
cftime: 1.6.5
nc_time_axis: None
iris: None
bottleneck: None
dask: None
distributed: None
matplotlib: 3.10.0
cartopy: None
seaborn: 0.13.2
numbagg: None
fsspec: 2025.5.1
cupy: None
pint: None
sparse: None
flox: None
numpy_groupies: None
setuptools: 78.1.1
pip: 25.3
conda: None
pytest: None
mypy: None
IPython: 9.2.0
sphinx: None

</details>


Implementation patch:
diff --git a/xarray/plot/dataarray_plot.py b/xarray/plot/dataarray_plot.py
index 3a140ad46ca..c64e1453e16 100644
--- a/xarray/plot/dataarray_plot.py
+++ b/xarray/plot/dataarray_plot.py
@@ -641,7 +641,7 @@ def step(
 
 def hist(
     darray: DataArray,
-    *args: Any,
+    *,
     figsize: Iterable[float] | None = None,
     size: float | None = None,
     aspect: AspectOptions = None,
@@ -695,8 +695,6 @@ def hist(
         Additional keyword arguments to :py:func:`matplotlib:matplotlib.pyplot.hist`.
 
     """
-    assert len(args) == 0
-
     if darray.ndim == 0 or darray.size == 0:
         # TypeError to be consistent with pandas
         raise TypeError("No numeric data to plot.")
@@ -928,6 +926,7 @@ def newplotfunc(
                 raise ValueError(msg)
             else:
                 warnings.warn(msg, DeprecationWarning, stacklevel=2)
+            del msg
         del args
 
         if hue_style is not None:
@@ -1461,6 +1460,7 @@ def newplotfunc(
                 raise ValueError(msg)
             else:
                 warnings.warn(msg, DeprecationWarning, stacklevel=2)
+            del msg
         del args
 
         # Decide on a default for the colorbar before facetgrids
diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py
index ff508ee213c..bc5ed49e5c9 100644
--- a/xarray/plot/dataset_plot.py
+++ b/xarray/plot/dataset_plot.py
@@ -213,6 +213,7 @@ def newplotfunc(
                 raise ValueError(msg)
             else:
                 warnings.warn(msg, DeprecationWarning, stacklevel=2)
+            del msg
         del args
 
         _is_facetgrid = kwargs.pop("_is_facetgrid", False)
@@ -752,7 +753,7 @@ def _temp_dataarray(ds: Dataset, y: Hashable, locals_: dict[str, Any]) -> DataAr
 @overload
 def scatter(  # type: ignore[misc,unused-ignore]  # None is hashable :(
     ds: Dataset,
-    *args: Any,
+    *,
     x: Hashable | None = None,
     y: Hashable | None = None,
     z: Hashable | None = None,
@@ -793,7 +794,7 @@ def scatter(  # type: ignore[misc,unused-ignore]  # None is hashable :(
 @overload
 def scatter(
     ds: Dataset,
-    *args: Any,
+    *,
     x: Hashable | None = None,
     y: Hashable | None = None,
     z: Hashable | None = None,
@@ -834,7 +835,7 @@ def scatter(
 @overload
 def scatter(
     ds: Dataset,
-    *args: Any,
+    *,
     x: Hashable | None = None,
     y: Hashable | None = None,
     z: Hashable | None = None,
@@ -875,7 +876,7 @@ def scatter(
 @_update_doc_to_dataset(dataarray_plot.scatter)
 def scatter(
     ds: Dataset,
-    *args: Any,
+    *,
     x: Hashable | None = None,
     y: Hashable | None = None,
     z: Hashable | None = None,


Test patch:
diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py
index bb0b809448c..61d742a901b 100644
--- a/xarray/tests/test_plot.py
+++ b/xarray/tests/test_plot.py
@@ -834,6 +834,14 @@ def test_slice_in_title_single_item_array(self) -> None:
         title = plt.gca().get_title()
         assert "d = [10.009]" == title
 
+    def test_warns_for_few_positional_args(self) -> None:
+        with pytest.warns(DeprecationWarning, match="Using positional arguments"):
+            self.darray.plot.scatter("period")
+
+    def test_raises_for_too_many_positional_args(self) -> None:
+        with pytest.raises(ValueError, match="Using positional arguments"):
+            self.darray.plot.scatter("period", "foo", "bar", "blue", {})
+
 
 class TestPlotStep(PlotTestCase):
     @pytest.fixture(autouse=True)
@@ -1730,6 +1738,19 @@ def test_colormap_error_norm_and_vmin_vmax(self) -> None:
         with pytest.raises(ValueError):
             self.darray.plot(norm=norm, vmax=2)  # type: ignore[call-arg]
 
+    def test_plot_warns_for_2_positional_args(self) -> None:
+        da = xr.DataArray(
+            np.random.randn(2, 6, 6),
+            dims=("time", "x", "y"),
+            coords={"x": np.arange(6), "y": np.arange(6)},
+        )
+        with pytest.warns(DeprecationWarning, match="Using positional arguments"):
+            self.plotfunc(da, "x", "y", col="time")
+
+    def test_plot_raises_too_many_for_positional_args(self) -> None:
+        with pytest.raises(ValueError, match="Using positional arguments"):
+            self.plotmethod("x", "y", (12, 4))
+
 
 @pytest.mark.slow
 class TestContourf(Common2dMixin, PlotTestCase):
@@ -2890,6 +2911,10 @@ def test_bad_args(
                 x=x, y=y, hue=hue, add_legend=add_legend, add_colorbar=add_colorbar
             )
 
+    def test_does_not_allow_positional_args(self) -> None:
+        with pytest.raises(TypeError, match="takes 1 positional argument"):
+            self.ds.plot.scatter("A", "B")
+
     def test_datetime_hue(self) -> None:
         ds2 = self.ds.copy()