Repository: pandas-dev/pandas

Issue:
Format mismatch doesn't coerce to NaT
#### Code Sample, a copy-pastable example if possible

```python
>>> pd.to_datetime("00:01:18", format='H%:M%:S%', errors='coerce', infer_datetime_format=False)
"00:01:18"
```
#### Problem description

In this example code I have made a mistake with the format `'H%:M%:S%'` it should be `'%H:%M:%S'`
instead. Since I have specified errors to `'coerce'` getting a string as the return value here seems very surprising to me.

#### Expected Output

Given that I have specified `errors='coerce'` and `infer_datetime_format=False` I'd expect to get a `NaT` result here.

#### Output of ``pd.show_versions()``

<details>

[paste the output of ``pd.show_versions()`` here below this line]
INSTALLED VERSIONS
------------------
commit: None
python: 3.7.2.final.0
python-bits: 64
OS: Linux
OS-release: 4.15.0-1031-aws
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: C.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.23.4
pytest: 3.8.0
pip: 18.1
setuptools: 40.2.0
Cython: 0.28.5
numpy: 1.15.1
scipy: 1.1.0
pyarrow: None
xarray: None
IPython: 6.5.0
sphinx: 1.7.9
patsy: 0.5.0
dateutil: 2.7.3
pytz: 2018.5
blosc: None
bottleneck: 1.2.1
tables: 3.4.4
numexpr: 2.6.8
feather: None
matplotlib: 2.2.3
openpyxl: 2.5.6
xlrd: 1.1.0
xlwt: 1.3.0
xlsxwriter: 1.1.0
lxml: 4.2.5
bs4: 4.6.3
html5lib: 1.0.1
sqlalchemy: 1.2.11
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None
</details>



Discussion:

Resolving patch:
diff --git a/pandas/core/tools/datetimes.py b/pandas/core/tools/datetimes.py
--- a/pandas/core/tools/datetimes.py
+++ b/pandas/core/tools/datetimes.py
@@ -265,7 +265,12 @@ def _convert_listlike_datetimes(arg, box, format, name=None, tz=None,
                 except tslibs.OutOfBoundsDatetime:
                     if errors == 'raise':
                         raise
-                    result = arg
+                    elif errors == 'coerce':
+                        result = np.empty(arg.shape, dtype='M8[ns]')
+                        iresult = result.view('i8')
+                        iresult.fill(tslibs.iNaT)
+                    else:
+                        result = arg
                 except ValueError:
                     # if format was inferred, try falling back
                     # to array_to_datetime - terminate here
@@ -273,7 +278,12 @@ def _convert_listlike_datetimes(arg, box, format, name=None, tz=None,
                     if not infer_datetime_format:
                         if errors == 'raise':
                             raise
-                        result = arg
+                        elif errors == 'coerce':
+                            result = np.empty(arg.shape, dtype='M8[ns]')
+                            iresult = result.view('i8')
+                            iresult.fill(tslibs.iNaT)
+                        else:
+                            result = arg
         except ValueError as e:
             # Fallback to try to convert datetime objects if timezone-aware
             #  datetime objects are found without passing `utc=True`


Test patch:
diff --git a/pandas/tests/indexes/datetimes/test_tools.py b/pandas/tests/indexes/datetimes/test_tools.py
--- a/pandas/tests/indexes/datetimes/test_tools.py
+++ b/pandas/tests/indexes/datetimes/test_tools.py
@@ -555,6 +555,63 @@ def test_datetime_invalid_datatype(self):
         with pytest.raises(TypeError):
             pd.to_datetime(pd.to_datetime)
 
+    @pytest.mark.parametrize('value', ["a", "00:01:99"])
+    @pytest.mark.parametrize('infer', [True, False])
+    @pytest.mark.parametrize('format', [None, 'H%:M%:S%'])
+    def test_datetime_invalid_scalar(self, value, format, infer):
+        # GH24763
+        res = pd.to_datetime(value, errors='ignore', format=format,
+                             infer_datetime_format=infer)
+        assert res == value
+
+        res = pd.to_datetime(value, errors='coerce', format=format,
+                             infer_datetime_format=infer)
+        assert res is pd.NaT
+
+        with pytest.raises(ValueError):
+            pd.to_datetime(value, errors='raise', format=format,
+                           infer_datetime_format=infer)
+
+    @pytest.mark.parametrize('value', ["3000/12/11 00:00:00"])
+    @pytest.mark.parametrize('infer', [True, False])
+    @pytest.mark.parametrize('format', [None, 'H%:M%:S%'])
+    def test_datetime_outofbounds_scalar(self, value, format, infer):
+        # GH24763
+        res = pd.to_datetime(value, errors='ignore', format=format,
+                             infer_datetime_format=infer)
+        assert res == value
+
+        res = pd.to_datetime(value, errors='coerce', format=format,
+                             infer_datetime_format=infer)
+        assert res is pd.NaT
+
+        if format is not None:
+            with pytest.raises(ValueError):
+                pd.to_datetime(value, errors='raise', format=format,
+                               infer_datetime_format=infer)
+        else:
+            with pytest.raises(OutOfBoundsDatetime):
+                pd.to_datetime(value, errors='raise', format=format,
+                               infer_datetime_format=infer)
+
+    @pytest.mark.parametrize('values', [["a"], ["00:01:99"],
+                                        ["a", "b", "99:00:00"]])
+    @pytest.mark.parametrize('infer', [True, False])
+    @pytest.mark.parametrize('format', [None, 'H%:M%:S%'])
+    def test_datetime_invalid_index(self, values, format, infer):
+        # GH24763
+        res = pd.to_datetime(values, errors='ignore', format=format,
+                             infer_datetime_format=infer)
+        tm.assert_index_equal(res, pd.Index(values))
+
+        res = pd.to_datetime(values, errors='coerce', format=format,
+                             infer_datetime_format=infer)
+        tm.assert_index_equal(res, pd.DatetimeIndex([pd.NaT] * len(values)))
+
+        with pytest.raises(ValueError):
+            pd.to_datetime(values, errors='raise', format=format,
+                           infer_datetime_format=infer)
+
     @pytest.mark.parametrize("utc", [True, None])
     @pytest.mark.parametrize("format", ['%Y%m%d %H:%M:%S', None])
     @pytest.mark.parametrize("box", [True, False])