Skip to content

Commit db54438

Browse files
authored
ENH: Change DataFrame.to_excel to output unformatted excel file (#54302)
* Updated default styling logic for to_excel and added unit tests. * Adding documentation to the Pandas User Guide. * Updating whatsnew * Fixing merge conflict. * Updating user guide documentation. * Fixing syntax error. * Updating implementation based on reviewer feedback. * Updating documentation.
1 parent 06a7251 commit db54438

File tree

4 files changed

+77
-23
lines changed

4 files changed

+77
-23
lines changed

doc/source/user_guide/io.rst

+14
Original file line numberDiff line numberDiff line change
@@ -3908,6 +3908,20 @@ The look and feel of Excel worksheets created from pandas can be modified using
39083908
* ``float_format`` : Format string for floating point numbers (default ``None``).
39093909
* ``freeze_panes`` : A tuple of two integers representing the bottommost row and rightmost column to freeze. Each of these parameters is one-based, so (1, 1) will freeze the first row and first column (default ``None``).
39103910

3911+
.. note::
3912+
3913+
As of Pandas 3.0, by default spreadsheets created with the ``to_excel`` method
3914+
will not contain any styling. Users wishing to bold text, add bordered styles,
3915+
etc in a worksheet output by ``to_excel`` can do so by using :meth:`Styler.to_excel`
3916+
to create styled excel files. For documentation on styling spreadsheets, see
3917+
`here <https://pandas.pydata.org/docs/user_guide/style.html#Export-to-Excel>`__.
3918+
3919+
3920+
.. code-block:: python
3921+
3922+
css = "border: 1px solid black; font-weight: bold;"
3923+
df.style.map_index(lambda x: css).map_index(lambda x: css, axis=1).to_excel("myfile.xlsx")
3924+
39113925
Using the `Xlsxwriter`_ engine provides many options for controlling the
39123926
format of an Excel worksheet created with the ``to_excel`` method. Excellent examples can be found in the
39133927
`Xlsxwriter`_ documentation here: https://xlsxwriter.readthedocs.io/working_with_pandas.html

doc/source/whatsnew/v3.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ Other API changes
9191
- 3rd party ``py.path`` objects are no longer explicitly supported in IO methods. Use :py:class:`pathlib.Path` objects instead (:issue:`57091`)
9292
- :attr:`MultiIndex.codes`, :attr:`MultiIndex.levels`, and :attr:`MultiIndex.names` now returns a ``tuple`` instead of a ``FrozenList`` (:issue:`53531`)
9393
- Made ``dtype`` a required argument in :meth:`ExtensionArray._from_sequence_of_strings` (:issue:`56519`)
94+
- Updated :meth:`DataFrame.to_excel` so that the output spreadsheet has no styling. Custom styling can still be done using :meth:`Styler.to_excel` (:issue:`54154`)
9495
- pickle and HDF (``.h5``) files created with Python 2 are no longer explicitly supported (:issue:`57387`)
9596
-
9697

pandas/io/formats/excel.py

+10-23
Original file line numberDiff line numberDiff line change
@@ -582,19 +582,6 @@ def __init__(
582582
self.merge_cells = merge_cells
583583
self.inf_rep = inf_rep
584584

585-
@property
586-
def header_style(self) -> dict[str, dict[str, str | bool]]:
587-
return {
588-
"font": {"bold": True},
589-
"borders": {
590-
"top": "thin",
591-
"right": "thin",
592-
"bottom": "thin",
593-
"left": "thin",
594-
},
595-
"alignment": {"horizontal": "center", "vertical": "top"},
596-
}
597-
598585
def _format_value(self, val):
599586
if is_scalar(val) and missing.isna(val):
600587
val = self.na_rep
@@ -642,7 +629,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
642629
row=lnum,
643630
col=coloffset,
644631
val=name,
645-
style=self.header_style,
632+
style=None,
646633
)
647634

648635
for lnum, (spans, levels, level_codes) in enumerate(
@@ -657,7 +644,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
657644
row=lnum,
658645
col=coloffset + i + 1,
659646
val=values[i],
660-
style=self.header_style,
647+
style=None,
661648
css_styles=getattr(self.styler, "ctx_columns", None),
662649
css_row=lnum,
663650
css_col=i,
@@ -673,7 +660,7 @@ def _format_header_mi(self) -> Iterable[ExcelCell]:
673660
row=lnum,
674661
col=coloffset + i + 1,
675662
val=v,
676-
style=self.header_style,
663+
style=None,
677664
css_styles=getattr(self.styler, "ctx_columns", None),
678665
css_row=lnum,
679666
css_col=i,
@@ -706,7 +693,7 @@ def _format_header_regular(self) -> Iterable[ExcelCell]:
706693
row=self.rowcounter,
707694
col=colindex + coloffset,
708695
val=colname,
709-
style=self.header_style,
696+
style=None,
710697
css_styles=getattr(self.styler, "ctx_columns", None),
711698
css_row=0,
712699
css_col=colindex,
@@ -729,7 +716,7 @@ def _format_header(self) -> Iterable[ExcelCell]:
729716
] * len(self.columns)
730717
if functools.reduce(lambda x, y: x and y, (x != "" for x in row)):
731718
gen2 = (
732-
ExcelCell(self.rowcounter, colindex, val, self.header_style)
719+
ExcelCell(self.rowcounter, colindex, val, None)
733720
for colindex, val in enumerate(row)
734721
)
735722
self.rowcounter += 1
@@ -763,7 +750,7 @@ def _format_regular_rows(self) -> Iterable[ExcelCell]:
763750
self.rowcounter += 1
764751

765752
if index_label and self.header is not False:
766-
yield ExcelCell(self.rowcounter - 1, 0, index_label, self.header_style)
753+
yield ExcelCell(self.rowcounter - 1, 0, index_label, None)
767754

768755
# write index_values
769756
index_values = self.df.index
@@ -775,7 +762,7 @@ def _format_regular_rows(self) -> Iterable[ExcelCell]:
775762
row=self.rowcounter + idx,
776763
col=0,
777764
val=idxval,
778-
style=self.header_style,
765+
style=None,
779766
css_styles=getattr(self.styler, "ctx_index", None),
780767
css_row=idx,
781768
css_col=0,
@@ -811,7 +798,7 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]:
811798
# if index labels are not empty go ahead and dump
812799
if com.any_not_none(*index_labels) and self.header is not False:
813800
for cidx, name in enumerate(index_labels):
814-
yield ExcelCell(self.rowcounter - 1, cidx, name, self.header_style)
801+
yield ExcelCell(self.rowcounter - 1, cidx, name, None)
815802

816803
if self.merge_cells:
817804
# Format hierarchical rows as merged cells.
@@ -838,7 +825,7 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]:
838825
row=self.rowcounter + i,
839826
col=gcolidx,
840827
val=values[i],
841-
style=self.header_style,
828+
style=None,
842829
css_styles=getattr(self.styler, "ctx_index", None),
843830
css_row=i,
844831
css_col=gcolidx,
@@ -856,7 +843,7 @@ def _format_hierarchical_rows(self) -> Iterable[ExcelCell]:
856843
row=self.rowcounter + idx,
857844
col=gcolidx,
858845
val=indexcolval,
859-
style=self.header_style,
846+
style=None,
860847
css_styles=getattr(self.styler, "ctx_index", None),
861848
css_row=idx,
862849
css_col=gcolidx,

pandas/tests/io/excel/test_style.py

+52
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,28 @@ def assert_equal_cell_styles(cell1, cell2):
3131
assert cell1.protection.__dict__ == cell2.protection.__dict__
3232

3333

34+
def test_styler_default_values():
35+
# GH 54154
36+
openpyxl = pytest.importorskip("openpyxl")
37+
df = DataFrame([{"A": 1, "B": 2, "C": 3}, {"A": 1, "B": 2, "C": 3}])
38+
39+
with tm.ensure_clean(".xlsx") as path:
40+
with ExcelWriter(path, engine="openpyxl") as writer:
41+
df.to_excel(writer, sheet_name="custom")
42+
43+
with contextlib.closing(openpyxl.load_workbook(path)) as wb:
44+
# Check font, spacing, indentation
45+
assert wb["custom"].cell(1, 1).font.bold is False
46+
assert wb["custom"].cell(1, 1).alignment.horizontal is None
47+
assert wb["custom"].cell(1, 1).alignment.vertical is None
48+
49+
# Check border
50+
assert wb["custom"].cell(1, 1).border.bottom.color is None
51+
assert wb["custom"].cell(1, 1).border.top.color is None
52+
assert wb["custom"].cell(1, 1).border.left.color is None
53+
assert wb["custom"].cell(1, 1).border.right.color is None
54+
55+
3456
@pytest.mark.parametrize(
3557
"engine",
3658
["xlsxwriter", "openpyxl"],
@@ -123,6 +145,36 @@ def test_styler_to_excel_unstyled(engine):
123145
]
124146

125147

148+
def test_styler_custom_style():
149+
# GH 54154
150+
css_style = "background-color: #111222"
151+
openpyxl = pytest.importorskip("openpyxl")
152+
df = DataFrame([{"A": 1, "B": 2}, {"A": 1, "B": 2}])
153+
154+
with tm.ensure_clean(".xlsx") as path:
155+
with ExcelWriter(path, engine="openpyxl") as writer:
156+
styler = df.style.map(lambda x: css_style)
157+
styler.to_excel(writer, sheet_name="custom", index=False)
158+
159+
with contextlib.closing(openpyxl.load_workbook(path)) as wb:
160+
# Check font, spacing, indentation
161+
assert wb["custom"].cell(1, 1).font.bold is False
162+
assert wb["custom"].cell(1, 1).alignment.horizontal is None
163+
assert wb["custom"].cell(1, 1).alignment.vertical is None
164+
165+
# Check border
166+
assert wb["custom"].cell(1, 1).border.bottom.color is None
167+
assert wb["custom"].cell(1, 1).border.top.color is None
168+
assert wb["custom"].cell(1, 1).border.left.color is None
169+
assert wb["custom"].cell(1, 1).border.right.color is None
170+
171+
# Check background color
172+
assert wb["custom"].cell(2, 1).fill.fgColor.index == "00111222"
173+
assert wb["custom"].cell(3, 1).fill.fgColor.index == "00111222"
174+
assert wb["custom"].cell(2, 2).fill.fgColor.index == "00111222"
175+
assert wb["custom"].cell(3, 2).fill.fgColor.index == "00111222"
176+
177+
126178
@pytest.mark.parametrize(
127179
"engine",
128180
["xlsxwriter", "openpyxl"],

0 commit comments

Comments
 (0)