Repository: pydata/xarray

Issue:
xarray.merge exception : invalid type promotion
#### Code Sample

```python
import xarray
import pandas
import numpy

array1 = xarray.DataArray(
    [numpy.timedelta64('NaT')],
    dims=['time'],
    coords={'time': pandas.to_datetime(['2018-01-01'])},
    name='foo'
)

array2 = xarray.DataArray(
    [numpy.timedelta64(30, 's')],
    dims=['time'],
    coords={'time': pandas.to_datetime(['2018-01-02'])},
    name='foo'
)

xarray.merge([array1, array2])
```
#### Problem description

Merging arrays with identical dtypes should work ...

There's some issue with the `NaT` being interpreted as float64, so that `xarray.core.dtypes.result_type` thinks that those 2 arrays are not compatible..

It works with `xarray==0.10.0` and fails at `xarray==0.10.1`.
I've pin-pointed the issue to commit 2aa5b8a5c0945935.

Work-around in the mean time :
```python
xarray.merge([array1.astype(float), array2.astype(float)]).astype('timedelta64')
```

#### Expected Output

```python

# expected :
xarray.DataArray(
    [numpy.timedelta64('NaT'), numpy.timedelta64(30, 's')],
    dims=['time'],
    coords={'time': pandas.to_datetime(['2018-01-01', '2018-01-02'])},
    name='foo'
)
```

#### Output of ``xr.show_versions()``

<details>
INSTALLED VERSIONS
------------------
commit: None
python: 3.6.2.final.0
python-bits: 64
OS: Linux
OS-release: 3.13.0-142-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: fr_FR.UTF-8
LOCALE: fr_FR.UTF-8

xarray: 0.10.0+dev60.g2aa5b8a
pandas: 0.22.0
numpy: 1.14.0
scipy: None
netCDF4: 1.3.1
h5netcdf: None
h5py: None
Nio: None
zarr: None
bottleneck: None
cyordereddict: None
dask: None
distributed: None
matplotlib: 2.1.1
cartopy: None
seaborn: None
setuptools: 38.4.0
pip: 9.0.1
conda: None
pytest: 3.3.2
IPython: 6.2.1
sphinx: None
</details>


Thanks !

Discussion:
shoyer: @NotSqrt this is really close -- any interest in finishing this up? :)
NotSqrt: @shoyer Can you review those changes, please ? Thanks !

Patch:
diff --git a/xarray/core/alignment.py b/xarray/core/alignment.py
index b0d2a49c..f82ddef2 100644
--- a/xarray/core/alignment.py
+++ b/xarray/core/alignment.py
@@ -174,11 +174,14 @@ def deep_align(objects, join='inner', copy=True, indexes=None,
 
     This function is not public API.
     """
+    from .dataarray import DataArray
+    from .dataset import Dataset
+
     if indexes is None:
         indexes = {}
 
     def is_alignable(obj):
-        return hasattr(obj, 'indexes') and hasattr(obj, 'reindex')
+        return isinstance(obj, (DataArray, Dataset))
 
     positions = []
     keys = []
diff --git a/xarray/core/combine.py b/xarray/core/combine.py
index 430f0e56..f0cc025d 100644
--- a/xarray/core/combine.py
+++ b/xarray/core/combine.py
@@ -125,16 +125,17 @@ def _calc_concat_dim_coord(dim):
     Infer the dimension name and 1d coordinate variable (if appropriate)
     for concatenating along the new dimension.
     """
+    from .dataarray import DataArray
+
     if isinstance(dim, basestring):
         coord = None
-    elif not hasattr(dim, 'dims'):
-        # dim is not a DataArray or IndexVariable
+    elif not isinstance(dim, (DataArray, Variable)):
         dim_name = getattr(dim, 'name', None)
         if dim_name is None:
             dim_name = 'concat_dim'
         coord = IndexVariable(dim_name, dim)
         dim = dim_name
-    elif not hasattr(dim, 'name'):
+    elif not isinstance(dim, DataArray):
         coord = as_variable(dim).to_index_variable()
         dim, = coord.dims
     else:
diff --git a/xarray/core/common.py b/xarray/core/common.py
index 55aca5f5..280034a3 100644
--- a/xarray/core/common.py
+++ b/xarray/core/common.py
@@ -10,7 +10,7 @@ import pandas as pd
 from . import duck_array_ops, dtypes, formatting, ops
 from .arithmetic import SupportsArithmetic
 from .pycompat import OrderedDict, basestring, dask_array_type, suppress
-from .utils import Frozen, SortedKeysDict
+from .utils import either_dict_or_kwargs, Frozen, SortedKeysDict
 
 
 class ImplementsArrayReduce(object):
@@ -526,24 +526,24 @@ class DataWithCoords(SupportsArithmetic, AttrAccessMixin):
                                              'precision': precision,
                                              'include_lowest': include_lowest})
 
-    def rolling(self, min_periods=None, center=False, **windows):
+    def rolling(self, dim=None, min_periods=None, center=False, **dim_kwargs):
         """
         Rolling window object.
 
         Parameters
         ----------
+        dim: dict, optional
+            Mapping from the dimension name to create the rolling iterator
+            along (e.g. `time`) to its moving window size.
         min_periods : int, default None
             Minimum number of observations in window required to have a value
             (otherwise result is NA). The default, None, is equivalent to
             setting min_periods equal to the size of the window.
         center : boolean, default False
             Set the labels at the center of the window.
-        **windows : dim=window
-            dim : str
-                Name of the dimension to create the rolling iterator
-                along (e.g., `time`).
-            window : int
-                Size of the moving window.
+        **dim_kwargs : optional
+            The keyword arguments form of ``dim``.
+            One of dim or dim_kwarg must be provided.
 
         Returns
         -------
@@ -582,9 +582,9 @@ class DataWithCoords(SupportsArithmetic, AttrAccessMixin):
         core.rolling.DataArrayRolling
         core.rolling.DatasetRolling
         """
-
-        return self._rolling_cls(self, min_periods=min_periods,
-                                 center=center, **windows)
+        dim = either_dict_or_kwargs(dim, dim_kwargs, 'rolling')
+        return self._rolling_cls(self, dim, min_periods=min_periods,
+                                 center=center)
 
     def resample(self, freq=None, dim=None, how=None, skipna=None,
                  closed=None, label=None, base=0, keep_attrs=False, **indexer):
@@ -650,6 +650,8 @@ class DataWithCoords(SupportsArithmetic, AttrAccessMixin):
 
         .. [1] http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
         """
+        # TODO support non-string indexer after removing the old API.
+
         from .dataarray import DataArray
         from .resample import RESAMPLE_DIM
 
diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py
index b1be9944..373a6a4c 100644
--- a/xarray/core/dataarray.py
+++ b/xarray/core/dataarray.py
@@ -779,9 +779,9 @@ class DataArray(AbstractArray, DataWithCoords):
         DataArray.isel
 
         """
-        indexers = either_dict_or_kwargs(indexers, indexers_kwargs, 'sel')
         ds = self._to_temp_dataset().sel(
-            indexers=indexers, drop=drop, method=method, tolerance=tolerance)
+            indexers=indexers, drop=drop, method=method, tolerance=tolerance,
+            **indexers_kwargs)
         return self._from_temp_dataset(ds)
 
     def isel_points(self, dim='points', **indexers):
@@ -1092,22 +1092,26 @@ class DataArray(AbstractArray, DataWithCoords):
         ds = self._to_temp_dataset().expand_dims(dim, axis)
         return self._from_temp_dataset(ds)
 
-    def set_index(self, append=False, inplace=False, **indexes):
+    def set_index(self, indexes=None, append=False, inplace=False,
+                  **indexes_kwargs):
         """Set DataArray (multi-)indexes using one or more existing
         coordinates.
 
         Parameters
         ----------
+        indexes : {dim: index, ...}
+            Mapping from names matching dimensions and values given
+            by (lists of) the names of existing coordinates or variables to set
+            as new (multi-)index.
         append : bool, optional
             If True, append the supplied index(es) to the existing index(es).
             Otherwise replace the existing index(es) (default).
         inplace : bool, optional
             If True, set new index(es) in-place. Otherwise, return a new
             DataArray object.
-        **indexes : {dim: index, ...}
-            Keyword arguments with names matching dimensions and values given
-            by (lists of) the names of existing coordinates or variables to set
-            as new (multi-)index.
+        **indexes_kwargs: optional
+            The keyword arguments form of ``indexes``.
+            One of indexes or indexes_kwargs must be provided.
 
         Returns
         -------
@@ -1118,6 +1122,7 @@ class DataArray(AbstractArray, DataWithCoords):
         --------
         DataArray.reset_index
         """
+        indexes = either_dict_or_kwargs(indexes, indexes_kwargs, 'set_index')
         coords, _ = merge_indexes(indexes, self._coords, set(), append=append)
         if inplace:
             self._coords = coords
@@ -1156,18 +1161,22 @@ class DataArray(AbstractArray, DataWithCoords):
         else:
             return self._replace(coords=coords)
 
-    def reorder_levels(self, inplace=False, **dim_order):
+    def reorder_levels(self, dim_order=None, inplace=False,
+                       **dim_order_kwargs):
         """Rearrange index levels using input order.
 
         Parameters
         ----------
+        dim_order : optional
+            Mapping from names matching dimensions and values given
+            by lists representing new level orders. Every given dimension
+            must have a multi-index.
         inplace : bool, optional
             If True, modify the dataarray in-place. Otherwise, return a new
             DataArray object.
-        **dim_order : optional
-            Keyword arguments with names matching dimensions and values given
-            by lists representing new level orders. Every given dimension
-            must have a multi-index.
+        **dim_order_kwargs: optional
+            The keyword arguments form of ``dim_order``.
+            One of dim_order or dim_order_kwargs must be provided.
 
         Returns
         -------
@@ -1175,6 +1184,8 @@ class DataArray(AbstractArray, DataWithCoords):
             Another dataarray, with this dataarray's data but replaced
             coordinates.
         """
+        dim_order = either_dict_or_kwargs(dim_order, dim_order_kwargs,
+                                          'reorder_levels')
         replace_coords = {}
         for dim, order in dim_order.items():
             coord = self._coords[dim]
@@ -1190,7 +1201,7 @@ class DataArray(AbstractArray, DataWithCoords):
         else:
             return self._replace(coords=coords)
 
-    def stack(self, **dimensions):
+    def stack(self, dimensions=None, **dimensions_kwargs):
         """
         Stack any number of existing dimensions into a single new dimension.
 
@@ -1199,9 +1210,12 @@ class DataArray(AbstractArray, DataWithCoords):
 
         Parameters
         ----------
-        **dimensions : keyword arguments of the form new_name=(dim1, dim2, ...)
+        dimensions : Mapping of the form new_name=(dim1, dim2, ...)
             Names of new dimensions, and the existing dimensions that they
             replace.
+        **dimensions_kwargs:
+            The keyword arguments form of ``dimensions``.
+            One of dimensions or dimensions_kwargs must be provided.
 
         Returns
         -------
@@ -1230,7 +1244,7 @@ class DataArray(AbstractArray, DataWithCoords):
         --------
         DataArray.unstack
         """
-        ds = self._to_temp_dataset().stack(**dimensions)
+        ds = self._to_temp_dataset().stack(dimensions, **dimensions_kwargs)
         return self._from_temp_dataset(ds)
 
     def unstack(self, dim):
@@ -1856,7 +1870,7 @@ class DataArray(AbstractArray, DataWithCoords):
         def func(self, other):
             if isinstance(other, (Dataset, groupby.GroupBy)):
                 return NotImplemented
-            if hasattr(other, 'indexes'):
+            if isinstance(other, DataArray):
                 align_type = (OPTIONS['arithmetic_join']
                               if join is None else join)
                 self, other = align(self, other, join=align_type, copy=False)
@@ -1978,7 +1992,7 @@ class DataArray(AbstractArray, DataWithCoords):
         ds = self._to_temp_dataset().diff(n=n, dim=dim, label=label)
         return self._from_temp_dataset(ds)
 
-    def shift(self, **shifts):
+    def shift(self, shifts=None, **shifts_kwargs):
         """Shift this array by an offset along one or more dimensions.
 
         Only the data is moved; coordinates stay in place. Values shifted from
@@ -1987,10 +2001,13 @@ class DataArray(AbstractArray, DataWithCoords):
 
         Parameters
         ----------
-        **shifts : keyword arguments of the form {dim: offset}
+        shifts : Mapping with the form of {dim: offset}
             Integer offset to shift along each of the given dimensions.
             Positive offsets shift to the right; negative offsets shift to the
             left.
+        **shifts_kwargs:
+            The keyword arguments form of ``shifts``.
+            One of shifts or shifts_kwarg must be provided.
 
         Returns
         -------
@@ -2012,8 +2029,8 @@ class DataArray(AbstractArray, DataWithCoords):
         Coordinates:
           * x        (x) int64 0 1 2
         """
-        variable = self.variable.shift(**shifts)
-        return self._replace(variable)
+        ds = self._to_temp_dataset().shift(shifts=shifts, **shifts_kwargs)
+        return self._from_temp_dataset(ds)
 
     def roll(self, shifts=None, roll_coords=None, **shifts_kwargs):
         """Roll this array by an offset along one or more dimensions.
diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py
index 37544aca..3d02b382 100644
--- a/xarray/core/dataset.py
+++ b/xarray/core/dataset.py
@@ -1759,8 +1759,8 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
         align
         """
         indexers = alignment.reindex_like_indexers(self, other)
-        return self.reindex(method=method, copy=copy, tolerance=tolerance,
-                            **indexers)
+        return self.reindex(indexers=indexers, method=method, copy=copy,
+                            tolerance=tolerance)
 
     def reindex(self, indexers=None, method=None, tolerance=None, copy=True,
                 **indexers_kwargs):
@@ -1809,7 +1809,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
         pandas.Index.get_indexer
         """
         indexers = utils.either_dict_or_kwargs(indexers, indexers_kwargs,
-                                                 'reindex')
+                                               'reindex')
 
         bad_dims = [d for d in indexers if d not in self.dims]
         if bad_dims:
@@ -2144,22 +2144,26 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
 
         return self._replace_vars_and_dims(variables, self._coord_names)
 
-    def set_index(self, append=False, inplace=False, **indexes):
+    def set_index(self, indexes=None, append=False, inplace=False,
+                  **indexes_kwargs):
         """Set Dataset (multi-)indexes using one or more existing coordinates or
         variables.
 
         Parameters
         ----------
+        indexes : {dim: index, ...}
+            Mapping from names matching dimensions and values given
+            by (lists of) the names of existing coordinates or variables to set
+            as new (multi-)index.
         append : bool, optional
             If True, append the supplied index(es) to the existing index(es).
             Otherwise replace the existing index(es) (default).
         inplace : bool, optional
             If True, set new index(es) in-place. Otherwise, return a new
             Dataset object.
-        **indexes : {dim: index, ...}
-            Keyword arguments with names matching dimensions and values given
-            by (lists of) the names of existing coordinates or variables to set
-            as new (multi-)index.
+        **indexes_kwargs: optional
+            The keyword arguments form of ``indexes``.
+            One of indexes or indexes_kwargs must be provided.
 
         Returns
         -------
@@ -2170,6 +2174,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
         --------
         Dataset.reset_index
         """
+        indexes = either_dict_or_kwargs(indexes, indexes_kwargs, 'set_index')
         variables, coord_names = merge_indexes(indexes, self._variables,
                                                self._coord_names,
                                                append=append)
@@ -2206,18 +2211,22 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
         return self._replace_vars_and_dims(variables, coord_names=coord_names,
                                            inplace=inplace)
 
-    def reorder_levels(self, inplace=False, **dim_order):
+    def reorder_levels(self, dim_order=None, inplace=False,
+                       **dim_order_kwargs):
         """Rearrange index levels using input order.
 
         Parameters
         ----------
+        dim_order : optional
+            Mapping from names matching dimensions and values given
+            by lists representing new level orders. Every given dimension
+            must have a multi-index.
         inplace : bool, optional
             If True, modify the dataset in-place. Otherwise, return a new
             DataArray object.
-        **dim_order : optional
-            Keyword arguments with names matching dimensions and values given
-            by lists representing new level orders. Every given dimension
-            must have a multi-index.
+        **dim_order_kwargs: optional
+            The keyword arguments form of ``dim_order``.
+            One of dim_order or dim_order_kwargs must be provided.
 
         Returns
         -------
@@ -2225,6 +2234,8 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
             Another dataset, with this dataset's data but replaced
             coordinates.
         """
+        dim_order = either_dict_or_kwargs(dim_order, dim_order_kwargs,
+                                          'reorder_levels')
         replace_variables = {}
         for dim, order in dim_order.items():
             coord = self._variables[dim]
@@ -2253,7 +2264,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
 
         # consider dropping levels that are unused?
         levels = [self.get_index(dim) for dim in dims]
-        if hasattr(pd, 'RangeIndex'):
+        if LooseVersion(pd.__version__) < LooseVersion('0.19.0'):
             # RangeIndex levels in a MultiIndex are broken for appending in
             # pandas before v0.19.0
             levels = [pd.Int64Index(level)
@@ -2267,7 +2278,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
 
         return self._replace_vars_and_dims(variables, coord_names)
 
-    def stack(self, **dimensions):
+    def stack(self, dimensions=None, **dimensions_kwargs):
         """
         Stack any number of existing dimensions into a single new dimension.
 
@@ -2276,9 +2287,12 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
 
         Parameters
         ----------
-        **dimensions : keyword arguments of the form new_name=(dim1, dim2, ...)
+        dimensions : Mapping of the form new_name=(dim1, dim2, ...)
             Names of new dimensions, and the existing dimensions that they
             replace.
+        **dimensions_kwargs:
+            The keyword arguments form of ``dimensions``.
+            One of dimensions or dimensions_kwargs must be provided.
 
         Returns
         -------
@@ -2289,6 +2303,8 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
         --------
         Dataset.unstack
         """
+        dimensions = either_dict_or_kwargs(dimensions, dimensions_kwargs,
+                                           'stack')
         result = self
         for new_dim, dims in dimensions.items():
             result = result._stack_once(dims, new_dim)
@@ -2329,7 +2345,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
         if index.equals(full_idx):
             obj = self
         else:
-            obj = self.reindex(copy=False, **{dim: full_idx})
+            obj = self.reindex({dim: full_idx}, copy=False)
 
         new_dim_names = index.names
         new_dim_sizes = [lev.size for lev in index.levels]
@@ -2339,7 +2355,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
             if name != dim:
                 if dim in var.dims:
                     new_dims = OrderedDict(zip(new_dim_names, new_dim_sizes))
-                    variables[name] = var.unstack(**{dim: new_dims})
+                    variables[name] = var.unstack({dim: new_dims})
                 else:
                     variables[name] = var
 
@@ -2578,7 +2594,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
         else:
             raise TypeError('must specify how or thresh')
 
-        return self.isel(**{dim: mask})
+        return self.isel({dim: mask})
 
     def fillna(self, value):
         """Fill missing values in this object.
@@ -2843,17 +2859,20 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
         attrs = self.attrs if keep_attrs else None
         return type(self)(variables, attrs=attrs)
 
-    def assign(self, **kwargs):
+    def assign(self, variables=None, **variables_kwargs):
         """Assign new data variables to a Dataset, returning a new object
         with all the original variables in addition to the new ones.
 
         Parameters
         ----------
-        kwargs : keyword, value pairs
-            keywords are the variables names. If the values are callable, they
-            are computed on the Dataset and assigned to new data variables. If
-            the values are not callable, (e.g. a DataArray, scalar, or array),
-            they are simply assigned.
+        variables : mapping, value pairs
+            Mapping from variables names to the new values. If the new values
+            are callable, they are computed on the Dataset and assigned to new
+            data variables. If the values are not callable, (e.g. a DataArray,
+            scalar, or array), they are simply assigned.
+        **variables_kwargs:
+            The keyword arguments form of ``variables``.
+            One of variables or variables_kwarg must be provided.
 
         Returns
         -------
@@ -2873,9 +2892,10 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
         --------
         pandas.DataFrame.assign
         """
+        variables = either_dict_or_kwargs(variables, variables_kwargs, 'assign')
         data = self.copy()
         # do all calculations first...
-        results = data._calc_assign_results(kwargs)
+        results = data._calc_assign_results(variables)
         # ... and then assign
         data.update(results)
         return data
@@ -3155,10 +3175,12 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
     def _binary_op(f, reflexive=False, join=None):
         @functools.wraps(f)
         def func(self, other):
+            from .dataarray import DataArray
+
             if isinstance(other, groupby.GroupBy):
                 return NotImplemented
             align_type = OPTIONS['arithmetic_join'] if join is None else join
-            if hasattr(other, 'indexes'):
+            if isinstance(other, (DataArray, Dataset)):
                 self, other = align(self, other, join=align_type, copy=False)
             g = f if not reflexive else lambda x, y: f(y, x)
             ds = self._calculate_binary_op(g, other, join=align_type)
@@ -3170,12 +3192,14 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
     def _inplace_binary_op(f):
         @functools.wraps(f)
         def func(self, other):
+            from .dataarray import DataArray
+
             if isinstance(other, groupby.GroupBy):
                 raise TypeError('in-place operations between a Dataset and '
                                 'a grouped object are not permitted')
             # we don't actually modify arrays in-place with in-place Dataset
             # arithmetic -- this lets us automatically align things
-            if hasattr(other, 'indexes'):
+            if isinstance(other, (DataArray, Dataset)):
                 other = other.reindex_like(self, copy=False)
             g = ops.inplace_to_noninplace_op(f)
             ds = self._calculate_binary_op(g, other, inplace=True)
@@ -3310,7 +3334,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
         else:
             return difference
 
-    def shift(self, **shifts):
+    def shift(self, shifts=None, **shifts_kwargs):
         """Shift this dataset by an offset along one or more dimensions.
 
         Only data variables are moved; coordinates stay in place. This is
@@ -3318,10 +3342,13 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
 
         Parameters
         ----------
-        **shifts : keyword arguments of the form {dim: offset}
+        shifts : Mapping with the form of {dim: offset}
             Integer offset to shift along each of the given dimensions.
             Positive offsets shift to the right; negative offsets shift to the
             left.
+        **shifts_kwargs:
+            The keyword arguments form of ``shifts``.
+            One of shifts or shifts_kwarg must be provided.
 
         Returns
         -------
@@ -3345,6 +3372,7 @@ class Dataset(Mapping, ImplementsDatasetReduce, DataWithCoords,
         Data variables:
             foo      (x) object nan nan 'a' 'b' 'c'
         """
+        shifts = either_dict_or_kwargs(shifts, shifts_kwargs, 'shift')
         invalid = [k for k in shifts if k not in self.dims]
         if invalid:
             raise ValueError("dimensions %r do not exist" % invalid)
diff --git a/xarray/core/dtypes.py b/xarray/core/dtypes.py
index 7ad44472..a2f11728 100644
--- a/xarray/core/dtypes.py
+++ b/xarray/core/dtypes.py
@@ -80,6 +80,11 @@ def maybe_promote(dtype):
     # N.B. these casting rules should match pandas
     if np.issubdtype(dtype, np.floating):
         fill_value = np.nan
+    elif np.issubdtype(dtype, np.timedelta64):
+        # See https://github.com/numpy/numpy/issues/10685
+        # np.timedelta64 is a subclass of np.integer
+        # Check np.timedelta64 before np.integer
+        fill_value = np.timedelta64('NaT')
     elif np.issubdtype(dtype, np.integer):
         if dtype.itemsize <= 2:
             dtype = np.float32
@@ -90,8 +95,6 @@ def maybe_promote(dtype):
         fill_value = np.nan + np.nan * 1j
     elif np.issubdtype(dtype, np.datetime64):
         fill_value = np.datetime64('NaT')
-    elif np.issubdtype(dtype, np.timedelta64):
-        fill_value = np.timedelta64('NaT')
     else:
         dtype = object
         fill_value = np.nan
diff --git a/xarray/core/merge.py b/xarray/core/merge.py
index f823717a..984dd2fa 100644
--- a/xarray/core/merge.py
+++ b/xarray/core/merge.py
@@ -190,10 +190,13 @@ def expand_variable_dicts(list_of_variable_dicts):
     an input's values. The values of each ordered dictionary are all
     xarray.Variable objects.
     """
+    from .dataarray import DataArray
+    from .dataset import Dataset
+
     var_dicts = []
 
     for variables in list_of_variable_dicts:
-        if hasattr(variables, 'variables'):  # duck-type Dataset
+        if isinstance(variables, Dataset):
             sanitized_vars = variables.variables
         else:
             # append coords to var_dicts before appending sanitized_vars,
@@ -201,7 +204,7 @@ def expand_variable_dicts(list_of_variable_dicts):
             sanitized_vars = OrderedDict()
 
             for name, var in variables.items():
-                if hasattr(var, '_coords'):  # duck-type DataArray
+                if isinstance(var, DataArray):
                     # use private API for speed
                     coords = var._coords.copy()
                     # explicitly overwritten variables should take precedence
@@ -232,17 +235,19 @@ def determine_coords(list_of_variable_dicts):
         All variable found in the input should appear in either the set of
         coordinate or non-coordinate names.
     """
+    from .dataarray import DataArray
+    from .dataset import Dataset
+
     coord_names = set()
     noncoord_names = set()
 
     for variables in list_of_variable_dicts:
-        if hasattr(variables, 'coords') and hasattr(variables, 'data_vars'):
-            # duck-type Dataset
+        if isinstance(variables, Dataset):
             coord_names.update(variables.coords)
             noncoord_names.update(variables.data_vars)
         else:
             for name, var in variables.items():
-                if hasattr(var, '_coords'):  # duck-type DataArray
+                if isinstance(var, DataArray):
                     coords = set(var._coords)  # use private API for speed
                     # explicitly overwritten variables should take precedence
                     coords.discard(name)
diff --git a/xarray/core/rolling.py b/xarray/core/rolling.py
index 24ed280b..883dbb34 100644
--- a/xarray/core/rolling.py
+++ b/xarray/core/rolling.py
@@ -44,7 +44,7 @@ class Rolling(object):
 
     _attributes = ['window', 'min_periods', 'center', 'dim']
 
-    def __init__(self, obj, min_periods=None, center=False, **windows):
+    def __init__(self, obj, windows, min_periods=None, center=False):
         """
         Moving window object.
 
@@ -52,18 +52,18 @@ class Rolling(object):
         ----------
         obj : Dataset or DataArray
             Object to window.
+        windows : A mapping from a dimension name to window size
+            dim : str
+                Name of the dimension to create the rolling iterator
+                along (e.g., `time`).
+            window : int
+                Size of the moving window.
         min_periods : int, default None
             Minimum number of observations in window required to have a value
             (otherwise result is NA). The default, None, is equivalent to
             setting min_periods equal to the size of the window.
         center : boolean, default False
             Set the labels at the center of the window.
-        **windows : dim=window
-            dim : str
-                Name of the dimension to create the rolling iterator
-                along (e.g., `time`).
-            window : int
-                Size of the moving window.
 
         Returns
         -------
@@ -115,7 +115,7 @@ class Rolling(object):
 
 
 class DataArrayRolling(Rolling):
-    def __init__(self, obj, min_periods=None, center=False, **windows):
+    def __init__(self, obj, windows, min_periods=None, center=False):
         """
         Moving window object for DataArray.
         You should use DataArray.rolling() method to construct this object
@@ -125,18 +125,18 @@ class DataArrayRolling(Rolling):
         ----------
         obj : DataArray
             Object to window.
+        windows : A mapping from a dimension name to window size
+            dim : str
+                Name of the dimension to create the rolling iterator
+                along (e.g., `time`).
+            window : int
+                Size of the moving window.
         min_periods : int, default None
             Minimum number of observations in window required to have a value
             (otherwise result is NA). The default, None, is equivalent to
             setting min_periods equal to the size of the window.
         center : boolean, default False
             Set the labels at the center of the window.
-        **windows : dim=window
-            dim : str
-                Name of the dimension to create the rolling iterator
-                along (e.g., `time`).
-            window : int
-                Size of the moving window.
 
         Returns
         -------
@@ -149,8 +149,8 @@ class DataArrayRolling(Rolling):
         Dataset.rolling
         Dataset.groupby
         """
-        super(DataArrayRolling, self).__init__(obj, min_periods=min_periods,
-                                               center=center, **windows)
+        super(DataArrayRolling, self).__init__(
+            obj, windows, min_periods=min_periods, center=center)
 
         self.window_labels = self.obj[self.dim]
 
@@ -321,7 +321,7 @@ class DataArrayRolling(Rolling):
 
 
 class DatasetRolling(Rolling):
-    def __init__(self, obj, min_periods=None, center=False, **windows):
+    def __init__(self, obj, windows, min_periods=None, center=False):
         """
         Moving window object for Dataset.
         You should use Dataset.rolling() method to construct this object
@@ -331,18 +331,18 @@ class DatasetRolling(Rolling):
         ----------
         obj : Dataset
             Object to window.
+        windows : A mapping from a dimension name to window size
+            dim : str
+                Name of the dimension to create the rolling iterator
+                along (e.g., `time`).
+            window : int
+                Size of the moving window.
         min_periods : int, default None
             Minimum number of observations in window required to have a value
             (otherwise result is NA). The default, None, is equivalent to
             setting min_periods equal to the size of the window.
         center : boolean, default False
             Set the labels at the center of the window.
-        **windows : dim=window
-            dim : str
-                Name of the dimension to create the rolling iterator
-                along (e.g., `time`).
-            window : int
-                Size of the moving window.
 
         Returns
         -------
@@ -355,8 +355,7 @@ class DatasetRolling(Rolling):
         Dataset.groupby
         DataArray.groupby
         """
-        super(DatasetRolling, self).__init__(obj,
-                                             min_periods, center, **windows)
+        super(DatasetRolling, self).__init__(obj, windows, min_periods, center)
         if self.dim not in self.obj.dims:
             raise KeyError(self.dim)
         # Keep each Rolling object as an OrderedDict
@@ -364,8 +363,8 @@ class DatasetRolling(Rolling):
         for key, da in self.obj.data_vars.items():
             # keeps rollings only for the dataset depending on slf.dim
             if self.dim in da.dims:
-                self.rollings[key] = DataArrayRolling(da, min_periods,
-                                                      center, **windows)
+                self.rollings[key] = DataArrayRolling(
+                    da, windows, min_periods, center)
 
     def reduce(self, func, **kwargs):
         """Reduce the items in this group by applying `func` along some
diff --git a/xarray/core/variable.py b/xarray/core/variable.py
index d9772407..33a093d0 100644
--- a/xarray/core/variable.py
+++ b/xarray/core/variable.py
@@ -64,22 +64,15 @@ def as_variable(obj, name=None):
         The newly created variable.
 
     """
+    from .dataarray import DataArray
+
     # TODO: consider extending this method to automatically handle Iris and
-    # pandas objects.
-    if hasattr(obj, 'variable'):
+    if isinstance(obj, DataArray):
         # extract the primary Variable from DataArrays
         obj = obj.variable
 
     if isinstance(obj, Variable):
         obj = obj.copy(deep=False)
-    elif hasattr(obj, 'dims') and (hasattr(obj, 'data') or
-                                   hasattr(obj, 'values')):
-        obj_data = getattr(obj, 'data', None)
-        if obj_data is None:
-            obj_data = getattr(obj, 'values')
-        obj = Variable(obj.dims, obj_data,
-                       getattr(obj, 'attrs', None),
-                       getattr(obj, 'encoding', None))
     elif isinstance(obj, tuple):
         try:
             obj = Variable(*obj)
@@ -877,7 +870,7 @@ class Variable(common.AbstractArray, arithmetic.SupportsArithmetic,
         numpy.squeeze
         """
         dims = common.get_squeeze_dims(self, dim)
-        return self.isel(**{d: 0 for d in dims})
+        return self.isel({d: 0 for d in dims})
 
     def _shift_one_dim(self, dim, count):
         axis = self.get_axis_num(dim)
@@ -919,36 +912,46 @@ class Variable(common.AbstractArray, arithmetic.SupportsArithmetic,
 
         return type(self)(self.dims, data, self._attrs, fastpath=True)
 
-    def shift(self, **shifts):
+    def shift(self, shifts=None, **shifts_kwargs):
         """
         Return a new Variable with shifted data.
 
         Parameters
         ----------
-        **shifts : keyword arguments of the form {dim: offset}
+        shifts : mapping of the form {dim: offset}
             Integer offset to shift along each of the given dimensions.
             Positive offsets shift to the right; negative offsets shift to the
             left.
+        **shifts_kwargs:
+            The keyword arguments form of ``shifts``.
+            One of shifts or shifts_kwarg must be provided.
 
         Returns
         -------
         shifted : Variable
             Variable with the same dimensions and attributes but shifted data.
         """
+        shifts = either_dict_or_kwargs(shifts, shifts_kwargs, 'shift')
         result = self
         for dim, count in shifts.items():
             result = result._shift_one_dim(dim, count)
         return result
 
-    def pad_with_fill_value(self, fill_value=dtypes.NA, **pad_widths):
+    def pad_with_fill_value(self, pad_widths=None, fill_value=dtypes.NA,
+                            **pad_widths_kwargs):
         """
         Return a new Variable with paddings.
 
         Parameters
         ----------
-        **pad_width: keyword arguments of the form {dim: (before, after)}
+        pad_width: Mapping of the form {dim: (before, after)}
             Number of values padded to the edges of each dimension.
+        **pad_widths_kwargs:
+            Keyword argument for pad_widths
         """
+        pad_widths = either_dict_or_kwargs(pad_widths, pad_widths_kwargs,
+                                           'pad')
+
         if fill_value is dtypes.NA:  # np.nan is passed
             dtype, fill_value = dtypes.maybe_promote(self.dtype)
         else:
@@ -1009,22 +1012,27 @@ class Variable(common.AbstractArray, arithmetic.SupportsArithmetic,
 
         return type(self)(self.dims, data, self._attrs, fastpath=True)
 
-    def roll(self, **shifts):
+    def roll(self, shifts=None, **shifts_kwargs):
         """
         Return a new Variable with rolld data.
 
         Parameters
         ----------
-        **shifts : keyword arguments of the form {dim: offset}
+        shifts : mapping of the form {dim: offset}
             Integer offset to roll along each of the given dimensions.
             Positive offsets roll to the right; negative offsets roll to the
             left.
+        **shifts_kwargs:
+            The keyword arguments form of ``shifts``.
+            One of shifts or shifts_kwarg must be provided.
 
         Returns
         -------
         shifted : Variable
             Variable with the same dimensions and attributes but rolled data.
         """
+        shifts = either_dict_or_kwargs(shifts, shifts_kwargs, 'roll')
+
         result = self
         for dim, count in shifts.items():
             result = result._roll_one_dim(dim, count)
@@ -1142,7 +1150,7 @@ class Variable(common.AbstractArray, arithmetic.SupportsArithmetic,
         return Variable(new_dims, new_data, self._attrs, self._encoding,
                         fastpath=True)
 
-    def stack(self, **dimensions):
+    def stack(self, dimensions=None, **dimensions_kwargs):
         """
         Stack any number of existing dimensions into a single new dimension.
 
@@ -1151,9 +1159,12 @@ class Variable(common.AbstractArray, arithmetic.SupportsArithmetic,
 
         Parameters
         ----------
-        **dimensions : keyword arguments of the form new_name=(dim1, dim2, ...)
+        dimensions : Mapping of form new_name=(dim1, dim2, ...)
             Names of new dimensions, and the existing dimensions that they
             replace.
+        **dimensions_kwargs:
+            The keyword arguments form of ``dimensions``.
+            One of dimensions or dimensions_kwargs must be provided.
 
         Returns
         -------
@@ -1164,6 +1175,8 @@ class Variable(common.AbstractArray, arithmetic.SupportsArithmetic,
         --------
         Variable.unstack
         """
+        dimensions = either_dict_or_kwargs(dimensions, dimensions_kwargs,
+                                           'stack')
         result = self
         for new_dim, dims in dimensions.items():
             result = result._stack_once(dims, new_dim)
@@ -1195,7 +1208,7 @@ class Variable(common.AbstractArray, arithmetic.SupportsArithmetic,
         return Variable(new_dims, new_data, self._attrs, self._encoding,
                         fastpath=True)
 
-    def unstack(self, **dimensions):
+    def unstack(self, dimensions=None, **dimensions_kwargs):
         """
         Unstack an existing dimension into multiple new dimensions.
 
@@ -1204,9 +1217,12 @@ class Variable(common.AbstractArray, arithmetic.SupportsArithmetic,
 
         Parameters
         ----------
-        **dimensions : keyword arguments of the form old_dim={dim1: size1, ...}
+        dimensions : mapping of the form old_dim={dim1: size1, ...}
             Names of existing dimensions, and the new dimensions and sizes
             that they map to.
+        **dimensions_kwargs:
+            The keyword arguments form of ``dimensions``.
+            One of dimensions or dimensions_kwargs must be provided.
 
         Returns
         -------
@@ -1217,6 +1233,8 @@ class Variable(common.AbstractArray, arithmetic.SupportsArithmetic,
         --------
         Variable.stack
         """
+        dimensions = either_dict_or_kwargs(dimensions, dimensions_kwargs,
+                                           'unstack')
         result = self
         for old_dim, dims in dimensions.items():
             result = result._unstack_once(dims, old_dim)


Test patch:
diff --git a/xarray/tests/test_backends.py b/xarray/tests/test_backends.py
index e6de50b9..38012252 100644
--- a/xarray/tests/test_backends.py
+++ b/xarray/tests/test_backends.py
@@ -1404,7 +1404,6 @@ class BaseZarrTest(CFEncodedDataTest):
             with self.roundtrip(ds_chunk4) as actual:
                 self.assertEqual((4,), actual['var1'].encoding['chunks'])
 
-
         # TODO: remove this failure once syncronized overlapping writes are
         # supported by xarray
         ds_chunk4['var1'].encoding.update({'chunks': 5})
diff --git a/xarray/tests/test_dtypes.py b/xarray/tests/test_dtypes.py
index 833df85f..292c60b4 100644
--- a/xarray/tests/test_dtypes.py
+++ b/xarray/tests/test_dtypes.py
@@ -50,3 +50,39 @@ def test_result_type_dask_array():
 def test_inf(obj):
     assert dtypes.INF > obj
     assert dtypes.NINF < obj
+
+
+@pytest.mark.parametrize("kind, expected", [
+    ('a', (np.dtype('O'), 'nan')),  # dtype('S')
+    ('b', (np.float32, 'nan')),  # dtype('int8')
+    ('B', (np.float32, 'nan')),  # dtype('uint8')
+    ('c', (np.dtype('O'), 'nan')),  # dtype('S1')
+    ('D', (np.complex128, '(nan+nanj)')),  # dtype('complex128')
+    ('d', (np.float64, 'nan')),  # dtype('float64')
+    ('e', (np.float16, 'nan')),  # dtype('float16')
+    ('F', (np.complex64, '(nan+nanj)')),  # dtype('complex64')
+    ('f', (np.float32, 'nan')),  # dtype('float32')
+    ('h', (np.float32, 'nan')),  # dtype('int16')
+    ('H', (np.float32, 'nan')),  # dtype('uint16')
+    ('i', (np.float64, 'nan')),  # dtype('int32')
+    ('I', (np.float64, 'nan')),  # dtype('uint32')
+    ('l', (np.float64, 'nan')),  # dtype('int64')
+    ('L', (np.float64, 'nan')),  # dtype('uint64')
+    ('m', (np.timedelta64, 'NaT')),  # dtype('<m8')
+    ('M', (np.datetime64, 'NaT')),  # dtype('<M8')
+    ('O', (np.dtype('O'), 'nan')),  # dtype('O')
+    ('p', (np.float64, 'nan')),  # dtype('int64')
+    ('P', (np.float64, 'nan')),  # dtype('uint64')
+    ('q', (np.float64, 'nan')),  # dtype('int64')
+    ('Q', (np.float64, 'nan')),  # dtype('uint64')
+    ('S', (np.dtype('O'), 'nan')),  # dtype('S')
+    ('U', (np.dtype('O'), 'nan')),  # dtype('<U')
+    ('V', (np.dtype('O'), 'nan')),  # dtype('V')
+])
+def test_maybe_promote(kind, expected):
+    # 'g': np.float128 is not tested : not available on all platforms
+    # 'G': np.complex256 is not tested : not available on all platforms
+
+    actual = dtypes.maybe_promote(np.dtype(kind))
+    assert actual[0] == expected[0]
+    assert str(actual[1]) == expected[1]
diff --git a/xarray/tests/test_variable.py b/xarray/tests/test_variable.py
index 3db5e6ad..a08f7262 100644
--- a/xarray/tests/test_variable.py
+++ b/xarray/tests/test_variable.py
@@ -1,7 +1,6 @@
 
 from __future__ import absolute_import, division, print_function
 
-from collections import namedtuple
 from copy import copy, deepcopy
 from datetime import datetime, timedelta
 from distutils.version import LooseVersion
@@ -938,21 +937,6 @@ class TestVariable(TestCase, VariableSubclassTestCases):
         assert not isinstance(ds['x'], Variable)
         assert isinstance(as_variable(ds['x']), Variable)
 
-        FakeVariable = namedtuple('FakeVariable', 'values dims')
-        fake_xarray = FakeVariable(expected.values, expected.dims)
-        assert_identical(expected, as_variable(fake_xarray))
-
-        FakeVariable = namedtuple('FakeVariable', 'data dims')
-        fake_xarray = FakeVariable(expected.data, expected.dims)
-        assert_identical(expected, as_variable(fake_xarray))
-
-        FakeVariable = namedtuple('FakeVariable',
-                                  'data values dims attrs encoding')
-        fake_xarray = FakeVariable(expected_extra.data, expected_extra.values,
-                                   expected_extra.dims, expected_extra.attrs,
-                                   expected_extra.encoding)
-        assert_identical(expected_extra, as_variable(fake_xarray))
-
         xarray_tuple = (expected_extra.dims, expected_extra.values,
                         expected_extra.attrs, expected_extra.encoding)
         assert_identical(expected_extra, as_variable(xarray_tuple))