Repository: pandas-dev/pandas
Pull request: BUG: Fix render as column name in to_excel
- [x] closes #34331
- [x] tests added / passed
- [x] passes `black pandas`
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry

Should I add a whats new enty? Under which section?

https://github.com/phofl/pandas/blob/34331_render_column_name_raises_error_in_to_excel/pandas/io/formats/excel.py#L389

The previous code checked if the `df` had a attribute `render`. This check was True, if there was a `DataFrame` given as input with a column `render`. I think that this check should only be True if a `Styler` object is given, so I added an `isinstace` check for `Styler`. 

Does the `Styler` object always an attribute `render`? If not, we would run into problems, if we get a `Styler` object with a column named `render` but without the `render` attribute. If a `Styler` object has always an attribute `render`, we could delete the `hasattr` check.

Edit: I changed the check to `not isinstance(df, DataFrame)` to avoid the jinja issues.

Problem statement:
BUG: writing dataframe to excel if one of the rows has name "render" fails
I try to write an dataframe to excel with excel. It fails if a row is called "render". 

Pandas version 1.0.3

Reproduce with:
s1 = pandas.Series(("1","2"),index=["a","b"])
s2 = pandas.Series(("3","4"),index=["b","b"])

df = pandas.DataFrame({"render":s1,"huhu":s2})
df.to_excel("/tmp/out.xlsx")

gives:

AttributeError                            Traceback (most recent call last)
<ipython-input-44-02936207a82a> in <module>
----> 1 df.to_excel("/tmp/out.xlsx")

/usr/local/anaconda3/envs/exoplanets/lib/python3.7/site-packages/pandas/core/generic.py in to_excel(self, excel_writer, sheet_name, na_rep, float_format, columns, header, index, index_label, startrow, startcol, engine, merge_cells, encoding, inf_rep, verbose, freeze_panes)
   2171             index_label=index_label,
   2172             merge_cells=merge_cells,
-> 2173             inf_rep=inf_rep,
   2174         )
   2175         formatter.write(

/usr/local/anaconda3/envs/exoplanets/lib/python3.7/site-packages/pandas/io/formats/excel.py in __init__(self, df, na_rep, float_format, cols, header, index, index_label, merge_cells, inf_rep, style_converter)
    384         if hasattr(df, "render"):
    385             self.styler = df
--> 386             df = df.data
    387             if style_converter is None:
    388                 style_converter = CSSToExcelConverter()

/usr/local/anaconda3/envs/exoplanets/lib/python3.7/site-packages/pandas/core/generic.py in __getattr__(self, name)
   5272             if self._info_axis._can_hold_identifiers_and_holds_name(name):
   5273                 return self[name]
-> 5274             return object.__getattribute__(self, name)
   5275 
   5276     def __setattr__(self, name: str, value) -> None:

AttributeError: 'DataFrame' object has no attribute 'data'

Hints from issue discussion:
I tested your example on master and got a different traceback

```
Traceback (most recent call last):
  File "/home/developer/.config/JetBrains/PyCharmCE2020.1/scratches/scratch_pandas.py", line 7, in <module>
    df = pd.DataFrame({"render":s1,"huhu":s2})
  File "/home/developer/PycharmProjects/pandas/pandas/core/frame.py", line 460, in __init__
    mgr = init_dict(data, index, columns, dtype=dtype)
  File "/home/developer/PycharmProjects/pandas/pandas/core/internals/construction.py", line 283, in init_dict
    return arrays_to_mgr(arrays, data_names, index, columns, dtype=dtype)
  File "/home/developer/PycharmProjects/pandas/pandas/core/internals/construction.py", line 83, in arrays_to_mgr
    arrays = _homogenize(arrays, index, dtype)
  File "/home/developer/PycharmProjects/pandas/pandas/core/internals/construction.py", line 340, in _homogenize
    val = val.reindex(index, copy=False)
  File "/home/developer/PycharmProjects/pandas/pandas/core/series.py", line 4204, in reindex
    return super().reindex(index=index, **kwargs)
  File "/home/developer/PycharmProjects/pandas/pandas/core/generic.py", line 4482, in reindex
    axes, level, limit, tolerance, method, fill_value, copy
  File "/home/developer/PycharmProjects/pandas/pandas/core/generic.py", line 4505, in _reindex_axes
    allow_dups=False,
  File "/home/developer/PycharmProjects/pandas/pandas/core/generic.py", line 4550, in _reindex_with_indexers
    copy=copy,
  File "/home/developer/PycharmProjects/pandas/pandas/core/internals/managers.py", line 1255, in reindex_indexer
    self.axes[axis]._can_reindex(indexer)
  File "/home/developer/PycharmProjects/pandas/pandas/core/indexes/base.py", line 3235, in _can_reindex
    raise ValueError("cannot reindex from a duplicate axis")
ValueError: cannot reindex from a duplicate axis

Process finished with exit code 1
```

You have a small error in your example, You have to exchange the first `b` with an `a` in your second series.

```
import pandas as pd


s1 = pd.Series(("1","2"),index=["a","b"])
s2 = pd.Series(("3","4"),index=["a","b"])

df = pd.DataFrame({"render":s1,"huhu":s2})
df.to_excel("/tmp/out.xlsx")
```
Correct, sorry made a copy and paste error into the bug report.

should be

s1 = pd.Series(("1","2"),index=["a","b"])
s2 = pd.Series(("3","4"),index=["a","b"])
to reproduce the error. 




> Am 23.05.2020 um 11:26 schrieb patrick <notifications@github.com <mailto:notifications@github.com>>:
> 
> 
> I tested your example on master and got a different traceback
> 
> Traceback (most recent call last):
>   File "/home/developer/.config/JetBrains/PyCharmCE2020.1/scratches/scratch_pandas.py", line 7, in <module>
>     df = pd.DataFrame({"render":s1,"huhu":s2})
>   File "/home/developer/PycharmProjects/pandas/pandas/core/frame.py", line 460, in __init__
>     mgr = init_dict(data, index, columns, dtype=dtype)
>   File "/home/developer/PycharmProjects/pandas/pandas/core/internals/construction.py", line 283, in init_dict
>     return arrays_to_mgr(arrays, data_names, index, columns, dtype=dtype)
>   File "/home/developer/PycharmProjects/pandas/pandas/core/internals/construction.py", line 83, in arrays_to_mgr
>     arrays = _homogenize(arrays, index, dtype)
>   File "/home/developer/PycharmProjects/pandas/pandas/core/internals/construction.py", line 340, in _homogenize
>     val = val.reindex(index, copy=False)
>   File "/home/developer/PycharmProjects/pandas/pandas/core/series.py", line 4204, in reindex
>     return super().reindex(index=index, **kwargs)
>   File "/home/developer/PycharmProjects/pandas/pandas/core/generic.py", line 4482, in reindex
>     axes, level, limit, tolerance, method, fill_value, copy
>   File "/home/developer/PycharmProjects/pandas/pandas/core/generic.py", line 4505, in _reindex_axes
>     allow_dups=False,
>   File "/home/developer/PycharmProjects/pandas/pandas/core/generic.py", line 4550, in _reindex_with_indexers
>     copy=copy,
>   File "/home/developer/PycharmProjects/pandas/pandas/core/internals/managers.py", line 1255, in reindex_indexer
>     self.axes[axis]._can_reindex(indexer)
>   File "/home/developer/PycharmProjects/pandas/pandas/core/indexes/base.py", line 3235, in _can_reindex
>     raise ValueError("cannot reindex from a duplicate axis")
> ValueError: cannot reindex from a duplicate axis
> 
> Process finished with exit code 1
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub <https://github.com/pandas-dev/pandas/issues/34331#issuecomment-633013792>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/ACI42UP6KNIFB5K35R2QWMLRS6JDPANCNFSM4NIJ4TTQ>.
>

Commit message: Delete hasattr check

Patch to review:
diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst
index 41d519e0765dc..ee7ead19faec9 100644
--- a/doc/source/whatsnew/v1.1.0.rst
+++ b/doc/source/whatsnew/v1.1.0.rst
@@ -794,6 +794,7 @@ I/O
 - Bug in :meth:`~DataFrame.read_feather` was raising an `ArrowIOError` when reading an s3 or http file path (:issue:`29055`)
 - Bug in :meth:`read_parquet` was raising a ``FileNotFoundError`` when passed an s3 directory path. (:issue:`26388`)
 - Bug in :meth:`~DataFrame.to_parquet` was throwing an ``AttributeError`` when writing a partitioned parquet file to s3 (:issue:`27596`)
+- Bug in :meth:`~DataFrame.to_excel` could not handle the column name `render` and was raising an ``KeyError`` (:issue:`34331`)
 
 Plotting
 ^^^^^^^^
diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py
index 4ec20977a5e7f..bf4586a4b5b96 100644
--- a/pandas/io/formats/excel.py
+++ b/pandas/io/formats/excel.py
@@ -16,7 +16,7 @@
 from pandas.core.dtypes.common import is_float, is_scalar
 from pandas.core.dtypes.generic import ABCIndex
 
-from pandas import Index, MultiIndex, PeriodIndex
+from pandas import DataFrame, Index, MultiIndex, PeriodIndex
 import pandas.core.common as com
 
 from pandas.io.common import stringify_path
@@ -385,7 +385,7 @@ def __init__(
     ):
         self.rowcounter = 0
         self.na_rep = na_rep
-        if hasattr(df, "render"):
+        if not isinstance(df, DataFrame):
             self.styler = df
             df = df.data
             if style_converter is None:
diff --git a/pandas/tests/io/excel/test_openpyxl.py b/pandas/tests/io/excel/test_openpyxl.py
index 5f8d58ea1f105..6168a2adaef22 100644
--- a/pandas/tests/io/excel/test_openpyxl.py
+++ b/pandas/tests/io/excel/test_openpyxl.py
@@ -107,10 +107,11 @@ def test_write_append_mode(ext, mode, expected):
             assert wb2.worksheets[index]["A1"].value == cell_value
 
 
-def test_to_excel_with_openpyxl_engine(ext, tmpdir):
+@pytest.mark.parametrize("col", ["B", "render"])
+def test_to_excel_with_openpyxl_engine(ext, tmpdir, col):
     # GH 29854
     df1 = DataFrame({"A": np.linspace(1, 10, 10)})
-    df2 = DataFrame({"B": np.linspace(1, 20, 10)})
+    df2 = DataFrame({col: np.linspace(1, 20, 10)})
     df = pd.concat([df1, df2], axis=1)
     styled = df.style.applymap(
         lambda val: "color: %s" % ("red" if val < 0 else "black")
diff --git a/pandas/tests/io/excel/test_writers.py b/pandas/tests/io/excel/test_writers.py
index 1692e1a8a0dd3..ff366036714e4 100644
--- a/pandas/tests/io/excel/test_writers.py
+++ b/pandas/tests/io/excel/test_writers.py
@@ -1201,6 +1201,14 @@ def test_write_lists_dict(self, path):
 
         tm.assert_frame_equal(read, expected)
 
+    def test_render_as_column_name(self, path):
+        # see gh-34331
+        df = DataFrame({"render": [1, 2], "data": [3, 4]})
+        df.to_excel(path, "Sheet1")
+        read = pd.read_excel(path, "Sheet1", index_col=0)
+        expected = df
+        tm.assert_frame_equal(read, expected)
+
     def test_true_and_false_value_options(self, path):
         # see gh-13347
         df = pd.DataFrame([["foo", "bar"]], columns=["col1", "col2"])


Code review comments:
diff_hunk: @@ -107,10 +107,11 @@ def test_write_append_mode(ext, mode, expected):
             assert wb2.worksheets[index]["A1"].value == cell_value
 
 
-def test_to_excel_with_openpyxl_engine(ext, tmpdir):
+@pytest.mark.parametrize("col", ["B", "render"])
+def test_to_excel_with_openpyxl_engine(ext, tmpdir, col):
     # GH 29854
     df1 = DataFrame({"A": np.linspace(1, 10, 10)})
-    df2 = DataFrame({"B": np.linspace(1, 20, 10)})
+    df2 = DataFrame({col: np.linspace(1, 20, 10)})
path: pandas/tests/io/excel/test_openpyxl.py
text: @user1:
does this actually test anything? i would simply paraterize df1

@author:
Took an old test because I wanted to avoid unnecessary code duplication and parametrized it. The test was previously added because of an openpyxl issue. I could revert the parametrization and add an own test, but I would like to avoid changing a previously existing test?

@user1:
you are welcome to add a new test

@author:
I changed the old one back and added a new one