BUG: to_html() with formatters=<list> and max_cols fixed
State: closed

- [x] closes #25955
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry

@hugoecarl
@guipleite


Labels:
name: Bug
name: IO HTML
description: read_html, to_html, Styler.apply, Styler.applymap

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





There are currently no PEP 8 issues detected in this Pull Request. Cheers! :beers: 

##### Comment last updated at 2019-09-11 00:08:07 UTC
Hey @simonjayhawkins , just fixed what you reviewed 👍 
Are we good here @WillAyd? I didn't follow the typing discussion.
`formatters_type` is a union of list and tuple (and dict) due to the isinstance check in `_get_formatter`. if that is changed to be less restrictive, the following could be a potential solution.

```diff
diff --git a/pandas/io/formats/format.py b/pandas/io/formats/format.py
index 5ce4bf6ea..0d3e57990 100644
--- a/pandas/io/formats/format.py
+++ b/pandas/io/formats/format.py
@@ -20,7 +20,9 @@ from typing import (
     Dict,
     Iterable,
     List,
+    Mapping,
     Optional,
+    Sequence,
     Tuple,
     Type,
     Union,
@@ -76,9 +78,7 @@ from pandas.io.formats.printing import adjoin, justify, pprint_thing
 if TYPE_CHECKING:
     from pandas import Series, DataFrame, Categorical
 
-formatters_type = Union[
-    List[Callable], Tuple[Callable, ...], Dict[Union[str, int], Callable]
-]
+formatters_type = Union[Sequence[Callable], Mapping[Union[str, int], Callable]]
 float_format_type = Union[str, Callable, "EngFormatter"]
 
 common_docstring = """
@@ -458,7 +458,7 @@ class TableFormatter:
         )
 
     def _get_formatter(self, i: Union[str, int]) -> Optional[Callable]:
-        if isinstance(self.formatters, (list, tuple)):
+        if isinstance(self.formatters, Sequence):
             if is_integer(i):
                 i = cast(int, i)
                 return self.formatters[i]
@@ -658,8 +658,8 @@ class DataFrameFormatter(TableFormatter):
                     (frame.iloc[:, :col_num], frame.iloc[:, -col_num:]), axis=1
                 )
                 # truncate formatter
-                if is_list_like(self.formatters) and self.formatters:
-                    truncate_fmt = cast(List[Callable], self.formatters)
+                if isinstance(self.formatters, Sequence):
+                    truncate_fmt = list(self.formatters)
                     self.formatters = truncate_fmt[:col_num] + truncate_fmt[-col_num:]
             self.tr_col_num = col_num
         if truncate_v:
```

in the docs, it states "List must be of length equal to the number of columns.". I don't think this is enforced and could causes issues with the truncated list if the length of the list (or sequence) doesn't match the number of columns.
Sorry, I haven't followed the discussion here but CI is passing.

Is this good to go, or is there an outstanding item @simonjayhawkins (something to do with validating lengths)?
@WillAyd @TomAugspurger The fix here slices from the end of the list of formatters. There is no check that the list of formatters is the same as the number of columns.  So IMO this solution is incomplete, but i'll defer if you think this could be a follow-up.
This raises on master

```python
In [14]: df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})

In [15]: df.to_html(formatters=['{}'.format])
IndexError: list index out of range
```

Does this PR introduce new cases where that will happen? With will happen with

```python
In [15]: df.to_html(formatters=['{}'.format], max_cols=1)
Out[15]: '<table border="1" class="dataframe">\n  <thead>\n    <tr style="text-align: right;">\n      ...
```

on your branch @gabriellm1?

If we aren't changing behavior here, then this still seems like a strict improvement and we'll do a followup.
Sorry if I got it in the wrong way. My branch happens exactly what you described. Can you explain what's the problem here? The first code should behave like the second one? If you guys want to resolve this here just explain what should I do or if you prefer another PR I can make it happen.
this better shows the incorrect behavior when the list is to short

![image](https://user-images.githubusercontent.com/13159005/64822912-0798f000-d5ae-11e9-9b40-f806fb51109a.png)

and when the list is too long

![image](https://user-images.githubusercontent.com/13159005/64822981-29927280-d5ae-11e9-8cb9-646e8d4e794f.png)



Hm, I see. So we need to check the size of formatters and raise an error when size is different?
> Hm, I see. So we need to check the size of formatters and raise an error when size is different?

I think we need to define a behavior. Does the length of `formatters` need to match the number of original columns, or the number of columns that actually get formatted (which is I think `min(len(df.columns), max_cols)`? 
> > Hm, I see. So we need to check the size of formatters and raise an error when size is different?
> 
> I think we need to define a behavior. Does the length of `formatters` need to match the number of original columns, or the number of columns that actually get formatted (which is I think `min(len(df.columns), max_cols)`?

If the length matches the number of displayed columns the truncation added here would be unnecessary. 

It would also be confusing for users, as i think the number of displayed columns in to_string (shares this code) depends on the terminal width and column contents?

so I would assume that the length of the formatters list should match the number of columns in the dataframe, as currently stated (but not enforced) in the api docs.
So which direction are we going?
Opened https://github.com/pandas-dev/pandas/issues/28469 as a followup. I think we're good here.
Thanks @gabriellm1!
Thanks for the assist guys !