Repository: pandas-dev/pandas

Issue:
Series/DataFrame.replace crashes without exception in some cases (concerning lists)
I have a Series of lists that I need to work with, and discovered some crashes without exceptions. Here's an overview of the behaviour (which is inconsistent to me).

```
import pandas as pd
import numpy as np
ser = pd.Series([['a', 'b'], np.nan, [1]])

ser.replace({np.nan : []}) # crashes w/o exception
ser.replace({np.nan : 'dummy'}) # works
# 0    [a, b]
# 1     dummy
# 2       [1]
# dtype: object

ser.replace({np.nan : ['dummy']}) # why does this unwrap?
# 0    [a, b]
# 1     dummy
# 2       [1]
# dtype: object

ser.replace({np.nan : ['dummy', 'alt']}) # crashes w/o exception

ser.fillna([]) # raises
ser.fillna({1 : []}) # this works!
# 0    [a, b]
# 1        []
# 2       [1]
# dtype: object

ser.fillna({1 : ['dummy', 'alt']}) # works as well!
# 0          [a, b]
# 1    [dummy, alt]
# 2             [1]
# dtype: object

# Dataframe has exact same behaviour as Series above
df = pd.DataFrame({'col' : ser})
df.replace({np.nan : []}) # crashes w/o exception
...
```

I agree that _interpreting_  a list as the argument to `.replace` makes no sense. But I don't understand why it's not possible to `fillna` a list (cf. other people asking this question https://stackoverflow.com/q/33199193/2965879).

There's no reason in my opinion why `.replace({np.nan : ['dummy', 'alt']})` or `.replace({np.nan : []})`couldn't work in principle - it's very clear what the intent is. Furthermore, it already works like that in `fillna` (of course with different interpretation of the dict-key). But even if forbidding lists is a design decision, the call shouldn't just crash, but raise an exception, at least.

In my case, I have to design an API that does replacements (as pre-/post-processing around actual work) with `pandas` in the background, and I'd like to be able to just pass through legal `dict`s to `Series.replace` / `DataFrame.replace` - e.g. `{r'\s*' : np.nan}` or `{np.nan : []}`. Otherwise I have to inspect every passed replacement parameter (with all the overhead that comes with allowing both `{search : replace}` and `{column : {search : replace}}`), extract special cases, and build complicated wrappers like in the answers of the above SO question.

Versions are the most recent on conda, details below.
#### Output of ``pd.show_versions()``
<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.4.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 78 Stepping 3, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.22.0
pytest: 3.2.1
pip: 9.0.1
setuptools: 38.4.0
Cython: 0.26.1
numpy: 1.13.3
scipy: 1.0.0
pyarrow: None
xarray: None
IPython: 6.2.1
sphinx: 1.6.3
patsy: 0.4.1
dateutil: 2.6.1
pytz: 2017.3
blosc: None
bottleneck: 1.2.1
tables: 3.4.2
numexpr: 2.6.4
feather: None
matplotlib: 2.1.0
openpyxl: 2.4.8
xlrd: 1.1.0
xlwt: 1.3.0
xlsxwriter: 1.0.2
lxml: 4.1.0
bs4: 4.6.0
html5lib: 1.0.1
sqlalchemy: 1.1.13
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>


Discussion:
@h-vetinari these are pretty non-idiomatic cases. If you want to have a look and submit a PR would be great.
happy to look at this issue and try to fix. reverting.

Resolving patch:
diff --git a/pandas/core/internals/blocks.py b/pandas/core/internals/blocks.py
--- a/pandas/core/internals/blocks.py
+++ b/pandas/core/internals/blocks.py
@@ -777,10 +777,9 @@ def copy(self, deep=True, mgr=None):
 
     def replace(self, to_replace, value, inplace=False, filter=None,
                 regex=False, convert=True, mgr=None):
-        """ replace the to_replace value with value, possible to create new
+        """replace the to_replace value with value, possible to create new
         blocks here this is just a call to putmask. regex is not used here.
-        It is used in ObjectBlocks.  It is here for API
-        compatibility.
+        It is used in ObjectBlocks.  It is here for API compatibility.
         """
 
         inplace = validate_bool_kwarg(inplace, 'inplace')
@@ -802,6 +801,11 @@ def replace(self, to_replace, value, inplace=False, filter=None,
                                     copy=not inplace) for b in blocks]
             return blocks
         except (TypeError, ValueError):
+            # GH 22083, TypeError or ValueError occurred within error handling
+            # causes infinite loop. Cast and retry only if not objectblock.
+            if is_object_dtype(self):
+                raise
+
             # try again with a compatible block
             block = self.astype(object)
             return block.replace(to_replace=original_to_replace,
diff --git a/pandas/core/missing.py b/pandas/core/missing.py
--- a/pandas/core/missing.py
+++ b/pandas/core/missing.py
@@ -68,6 +68,10 @@ def mask_missing(arr, values_to_mask):
         else:
             mask |= isna(arr)
 
+    # GH 21977
+    if mask is None:
+        mask = np.zeros(arr.shape, dtype=bool)
+
     return mask
 
 


Test patch:
diff --git a/pandas/tests/frame/test_replace.py b/pandas/tests/frame/test_replace.py
--- a/pandas/tests/frame/test_replace.py
+++ b/pandas/tests/frame/test_replace.py
@@ -603,6 +603,20 @@ def test_replace_list(self):
 
         assert_frame_equal(res, expec)
 
+    def test_replace_with_empty_list(self):
+        # GH 21977
+        s = pd.Series([['a', 'b'], [], np.nan, [1]])
+        df = pd.DataFrame({'col': s})
+        expected = df
+        result = df.replace([], np.nan)
+        assert_frame_equal(result, expected)
+
+        # GH 19266
+        with tm.assert_raises_regex(ValueError, "cannot assign mismatch"):
+            df.replace({np.nan: []})
+        with tm.assert_raises_regex(ValueError, "cannot assign mismatch"):
+            df.replace({np.nan: ['dummy', 'alt']})
+
     def test_replace_series_dict(self):
         # from GH 3064
         df = DataFrame({'zero': {'a': 0.0, 'b': 1}, 'one': {'a': 2.0, 'b': 0}})
diff --git a/pandas/tests/series/test_replace.py b/pandas/tests/series/test_replace.py
--- a/pandas/tests/series/test_replace.py
+++ b/pandas/tests/series/test_replace.py
@@ -130,6 +130,19 @@ def test_replace_with_single_list(self):
             s.replace([1, 2, 3], inplace=True, method='crash_cymbal')
         tm.assert_series_equal(s, ser)
 
+    def test_replace_with_empty_list(self):
+        # GH 21977
+        s = pd.Series([[1], [2, 3], [], np.nan, [4]])
+        expected = s
+        result = s.replace([], np.nan)
+        tm.assert_series_equal(result, expected)
+
+        # GH 19266
+        with tm.assert_raises_regex(ValueError, "cannot assign mismatch"):
+            s.replace({np.nan: []})
+        with tm.assert_raises_regex(ValueError, "cannot assign mismatch"):
+            s.replace({np.nan: ['dummy', 'alt']})
+
     def test_replace_mixed_types(self):
         s = pd.Series(np.arange(5), dtype='int64')