Repository: pydata/xarray

Issue:
xr.open_dataset(f1).to_netcdf(file2) is not idempotent
Here is the original (much truncated) file.
```
> ncdump ak.nc
netcdf ak {
dimensions:
        npts = UNLIMITED ; // (2 currently)
        ntimes = 4 ;
variables:
        short tmpk(npts, ntimes) ;
                tmpk:description = "2 m Temperature - closest to top of hour" ;
                tmpk:units = "K" ;
                tmpk:level = "2 m" ;
                tmpk:period_variable = "ntimes1" ;
                tmpk:missing_value = -9999s ;
                tmpk:scale_factor = 0.01 ;

// global attributes:
                :source = "ak-obs" ;
data:

 tmpk =
  26915, 27755, -9999, 27705,
  25595, -9999, 28315, -9999 ;
}
```
Python code:
```
ds = xr.open_dataset('ak.nc')
ds.to_netcdf('akbad.nc')
ds
ds['tmpk']

<xarray.Dataset>
Dimensions:  (npts: 2, ntimes: 4)
Dimensions without coordinates: npts, ntimes
Data variables:
    tmpk     (npts, ntimes) float32 ...
Attributes:
    source:   ak-obs

<xarray.DataArray 'tmpk' (npts: 2, ntimes: 4)>
array([[269.15, 277.55,    nan, 277.05],
       [255.95,    nan, 283.15,    nan]], dtype=float32)
Dimensions without coordinates: npts, ntimes
Attributes:
    description:      2 m Temperature - closest to top of hour
    units:            K
    level:            2 m
    period_variable:  ntimes1
```
File written to disk:
```
> ncdump akbad.nc
netcdf akbad {
dimensions:
        npts = UNLIMITED ; // (2 currently)
        ntimes = 4 ;
variables:
        short tmpk(npts, ntimes) ;
                tmpk:description = "2 m Temperature - closest to top of hour" ;
                tmpk:units = "K" ;
                tmpk:level = "2 m" ;
                tmpk:period_variable = "ntimes1" ;
                tmpk:scale_factor = 0.01 ;

// global attributes:
                :source = "ak-obs" ;
data:

 tmpk =
  26915, 27755, 0, 27705,
  25595, 0, 28315, 0 ;
}
```
To confuse matter more, I am getting a warning:
```
SerializationWarning: saving variable tmpk with floating point data as an integer dtype without any _FillValue to use for NaNs
```
This might make sense, since `tmpk` after decoding has datatype `float32`, however somehow the original variable type and `scale_factor` are preserved, but `missing_value` attribute disappears and value written back is wrong: 0. 
I want to write back `tmpk` as float number and let zlib worry about disk space.

xr.show_versions()
INSTALLED VERSIONS
------------------
commit: None
python: 3.7.1 | packaged by conda-forge | (default, Feb 18 2019, 01:42:00) 
[GCC 7.3.0]
python-bits: 64
OS: Linux
OS-release: 3.10.0-957.5.1.el7.x86_64
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8
libhdf5: 1.10.4
libnetcdf: 4.6.2

xarray: 0.12.0
pandas: 0.24.2
numpy: 1.15.4
scipy: 1.2.1
netCDF4: 1.5.0
pydap: None
h5netcdf: None
h5py: None
Nio: None
zarr: None
cftime: 1.0.3.4
nc_time_axis: None
PseudonetCDF: None
rasterio: None
cfgrib: None
iris: None
bottleneck: None
dask: 1.1.4
distributed: 1.26.0
matplotlib: 3.0.3
cartopy: 0.17.0
seaborn: 0.9.0
setuptools: 40.8.0
pip: 19.0.3
conda: 4.6.10
pytest: 4.3.1
IPython: 7.4.0
sphinx: 1.8.5

​

Discussion:
pep8speaks: Hello @dcherian! Thanks for opening this PR. We checked the lines you've touched for [PEP 8](https://www.python.org/dev/peps/pep-0008) issues, and found:



* In the file [`xarray/conventions.py`](https://github.com/pydata/xarray/blob/c96578fff9eea53182302d8315d5706a33ee9d4e/xarray/conventions.py):

> [Line 86:5](https://github.com/pydata/xarray/blob/c96578fff9eea53182302d8315d5706a33ee9d4e/xarray/conventions.py#L86): [E129](https://duckduckgo.com/?q=pep8%20E129) visually indented line with same indent as next logical line





Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index b01c53e7..f8ec1e08 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -72,6 +72,8 @@ Bug fixes
   By `Deepak Cherian <https://github.com/dcherian`_.
 - A deep copy deep-copies the coords (:issue:`1463`)
   By `Martin Pletcher <https://github.com/pletchm>`_.
+- Increased support for `missing_value` (:issue:`2871`)
+  By `Deepak Cherian <https://github.com/dcherian>`_.
 - Removed usages of `pytest.config`, which is deprecated (:issue:`2988`)
   By `Maximilian Roos <https://github.com/max-sixty>`_.
 - Fixed performance issues with cftime installed (:issue:`3000`)
diff --git a/xarray/coding/variables.py b/xarray/coding/variables.py
index 8f5ffe8a..c23e45e4 100644
--- a/xarray/coding/variables.py
+++ b/xarray/coding/variables.py
@@ -8,6 +8,7 @@ import pandas as pd
 
 from ..core import dtypes, duck_array_ops, indexing
 from ..core.pycompat import dask_array_type
+from ..core.utils import equivalent
 from ..core.variable import Variable
 
 
@@ -145,11 +146,24 @@ class CFMaskCoder(VariableCoder):
     def encode(self, variable, name=None):
         dims, data, attrs, encoding = unpack_for_encoding(variable)
 
-        if encoding.get('_FillValue') is not None:
+        fv = encoding.get('_FillValue')
+        mv = encoding.get('missing_value')
+
+        if fv is not None and mv is not None and not equivalent(fv, mv):
+            raise ValueError("Variable {!r} has multiple fill values {}. "
+                             "Cannot encode data. "
+                             .format(name, [fv, mv]))
+
+        if fv is not None:
             fill_value = pop_to(encoding, attrs, '_FillValue', name=name)
             if not pd.isnull(fill_value):
                 data = duck_array_ops.fillna(data, fill_value)
 
+        if mv is not None:
+            fill_value = pop_to(encoding, attrs, 'missing_value', name=name)
+            if not pd.isnull(fill_value) and fv is None:
+                data = duck_array_ops.fillna(data, fill_value)
+
         return Variable(dims, data, attrs, encoding)
 
     def decode(self, variable, name=None):
diff --git a/xarray/conventions.py b/xarray/conventions.py
index 5f41639e..3f8f76b0 100644
--- a/xarray/conventions.py
+++ b/xarray/conventions.py
@@ -82,7 +82,8 @@ def maybe_encode_nonstring_dtype(var, name=None):
         if dtype != var.dtype:
             if np.issubdtype(dtype, np.integer):
                 if (np.issubdtype(var.dtype, np.floating) and
-                        '_FillValue' not in var.attrs):
+                        '_FillValue' not in var.attrs and
+                        'missing_value' not in var.attrs):
                     warnings.warn('saving variable %s with floating '
                                   'point data as an integer dtype without '
                                   'any _FillValue to use for NaNs' % name,


Test patch:
diff --git a/xarray/tests/test_coding.py b/xarray/tests/test_coding.py
index 95c8ebc0..9f937ac7 100644
--- a/xarray/tests/test_coding.py
+++ b/xarray/tests/test_coding.py
@@ -6,7 +6,7 @@ import pytest
 import xarray as xr
 from xarray.coding import variables
 
-from . import assert_identical, requires_dask
+from . import assert_equal, assert_identical, requires_dask
 
 with suppress(ImportError):
     import dask.array as da
@@ -20,6 +20,23 @@ def test_CFMaskCoder_decode():
     assert_identical(expected, encoded)
 
 
+def test_CFMaskCoder_missing_value():
+    expected = xr.DataArray(np.array([[26915, 27755, -9999, 27705],
+                                      [25595, -9999, 28315, -9999]]),
+                            dims=['npts', 'ntimes'],
+                            name='tmpk')
+    expected.attrs['missing_value'] = -9999
+
+    decoded = xr.decode_cf(expected.to_dataset())
+    encoded, _ = xr.conventions.cf_encoder(decoded, decoded.attrs)
+
+    assert_equal(encoded['tmpk'], expected.variable)
+
+    decoded.tmpk.encoding['_FillValue'] = -9940
+    with pytest.raises(ValueError):
+        encoded, _ = xr.conventions.cf_encoder(decoded, decoded.attrs)
+
+
 @requires_dask
 def test_CFMaskCoder_decode_dask():
     original = xr.Variable(('x',), [0, -1, 1], {'_FillValue': -1}).chunk()