Repository: pandas-dev/pandas

Issue:
Pandas should validate display.max_rows option
#### Code Sample, a copy-pastable example if possible

```python
import pandas as pd
pd.set_option('display.max_rows', -1)
test = [['a', 'b'] ,['c','d'] ,['f','g']]
pd.DataFrame(test, columns=['columna', 'columnb'])
```
#### Problem description

This incorrectly print's 4 rows:

```
Out[1]:
..     ...     ...
   columna columnb
0        a       b
1        c       d
1        c       d
2        f       g

[3 rows x 2 columns]
```
Pandas should not allow negative numbers for this setting because it does non-sensical things. In many other places -1 is used to represent an unbounded number so setting it -1 is actually intuitive but it doesn't work correctly and we should fail fast instead of confusing users.

#### Expected Output

```
Out[1]:
..     ...     ...
   columna columnb
0        a       b
1        c       d
2        f       g

[3 rows x 2 columns]
```

#### Output of ``pd.show_versions()``

<details>

[paste the output of ``pd.show_versions()`` here below this line]
INSTALLED VERSIONS
------------------
commit: None
python: 3.6.3.candidate.1
python-bits: 64
OS: Linux
OS-release: 4.11.3-67_fbk17_4093_g2bf19e7a0b95
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: 
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.23.4
pytest: None
pip: None
setuptools: 40.4.3
Cython: 0.29a0
numpy: 1.15.1
scipy: 1.1.0
pyarrow: None
xarray: None
IPython: 6.5.0
sphinx: None
patsy: 0.4.1
dateutil: 2.7.3
pytz: 2018.4
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: 2.2.3
openpyxl: 2.4.11
xlrd: 0.9.4
xlwt: None
xlsxwriter: None
lxml: 3.5.0
bs4: None
html5lib: 0.9999999
sqlalchemy: 1.1.13
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None
</details>



Discussion:
That's a good idea.  We should check all such configuration options and see whether we can perform validation similar to what you are describing for `display.max_rows`.

Resolving patch:
diff --git a/pandas/_config/config.py b/pandas/_config/config.py
--- a/pandas/_config/config.py
+++ b/pandas/_config/config.py
@@ -787,6 +787,7 @@ def is_instance_factory(_type):
                 ValueError if x is not an instance of `_type`
 
     """
+
     if isinstance(_type, (tuple, list)):
         _type = tuple(_type)
         type_repr = "|".join(map(str, _type))
@@ -820,6 +821,32 @@ def inner(x):
     return inner
 
 
+def is_nonnegative_int(value):
+    """
+    Verify that value is None or a positive int.
+
+    Parameters
+    ----------
+    value : None or int
+            The `value` to be checked.
+
+    Raises
+    ------
+    ValueError
+        When the value is not None or is a negative integer
+    """
+
+    if value is None:
+        return
+
+    elif isinstance(value, int):
+        if value >= 0:
+            return
+
+    msg = "Value must be a nonnegative integer or None"
+    raise ValueError(msg)
+
+
 # common type validators, for convenience
 # usage: register_option(... , validator = is_int)
 is_int = is_type_factory(int)
diff --git a/pandas/core/config_init.py b/pandas/core/config_init.py
--- a/pandas/core/config_init.py
+++ b/pandas/core/config_init.py
@@ -17,6 +17,7 @@
     is_callable,
     is_instance_factory,
     is_int,
+    is_nonnegative_int,
     is_one_of_factory,
     is_text,
 )
@@ -319,7 +320,7 @@ def is_terminal():
 
 
 with cf.config_prefix("display"):
-    cf.register_option("precision", 6, pc_precision_doc, validator=is_int)
+    cf.register_option("precision", 6, pc_precision_doc, validator=is_nonnegative_int)
     cf.register_option(
         "float_format",
         None,
@@ -333,12 +334,7 @@ def is_terminal():
         pc_max_info_rows_doc,
         validator=is_instance_factory((int, type(None))),
     )
-    cf.register_option(
-        "max_rows",
-        60,
-        pc_max_rows_doc,
-        validator=is_instance_factory([type(None), int]),
-    )
+    cf.register_option("max_rows", 60, pc_max_rows_doc, validator=is_nonnegative_int)
     cf.register_option(
         "min_rows",
         10,
@@ -352,10 +348,7 @@ def is_terminal():
     else:
         max_cols = 20  # cannot determine optimal number of columns
     cf.register_option(
-        "max_columns",
-        max_cols,
-        pc_max_cols_doc,
-        validator=is_instance_factory([type(None), int]),
+        "max_columns", max_cols, pc_max_cols_doc, validator=is_nonnegative_int
     )
     cf.register_option(
         "large_repr",


Test patch:
diff --git a/pandas/tests/config/test_config.py b/pandas/tests/config/test_config.py
--- a/pandas/tests/config/test_config.py
+++ b/pandas/tests/config/test_config.py
@@ -208,13 +208,16 @@ def test_set_option_multiple(self):
 
     def test_validation(self):
         self.cf.register_option("a", 1, "doc", validator=self.cf.is_int)
+        self.cf.register_option("d", 1, "doc", validator=self.cf.is_nonnegative_int)
         self.cf.register_option("b.c", "hullo", "doc2", validator=self.cf.is_text)
+
         msg = "Value must have type '<class 'int'>'"
         with pytest.raises(ValueError, match=msg):
             self.cf.register_option("a.b.c.d2", "NO", "doc", validator=self.cf.is_int)
 
         self.cf.set_option("a", 2)  # int is_int
         self.cf.set_option("b.c", "wurld")  # str is_str
+        self.cf.set_option("d", 2)
 
         # None not is_int
         with pytest.raises(ValueError, match=msg):
@@ -222,6 +225,16 @@ def test_validation(self):
         with pytest.raises(ValueError, match=msg):
             self.cf.set_option("a", "ab")
 
+        msg = "Value must be a nonnegative integer or None"
+        with pytest.raises(ValueError, match=msg):
+            self.cf.register_option(
+                "a.b.c.d3", "NO", "doc", validator=self.cf.is_nonnegative_int
+            )
+        with pytest.raises(ValueError, match=msg):
+            self.cf.register_option(
+                "a.b.c.d3", -2, "doc", validator=self.cf.is_nonnegative_int
+            )
+
         msg = r"Value must be an instance of <class 'str'>\|<class 'bytes'>"
         with pytest.raises(ValueError, match=msg):
             self.cf.set_option("b.c", 1)