Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 90 additions & 98 deletions pandas/core/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@
from pandas.io.formats.printing import pprint_thing

if TYPE_CHECKING:
from collections.abc import Callable
from collections.abc import (
Callable,
Hashable,
Iterator,
Mapping,
Expand Down Expand Up @@ -2180,139 +2180,128 @@ def to_excel(
freeze_panes: tuple[int, int] | None = None,
storage_options: StorageOptions | None = None,
engine_kwargs: dict[str, Any] | None = None,
autofilter: bool = False,
) -> None:
"""
Write {klass} to an Excel sheet.
Write object to an Excel sheet.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the doc decorator on L2149

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also - it looks like there was a merge issue here and code got duplicated below.

Copy link
Author

@antznette1 antznette1 Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remove the doc decorator on L2149
@rhshadrach
It’s used for docstring parameter substitution. Removing it would break the docstring formatting used across pandas methods. The autofilter parameter is already documented in the Parameters section (around line 2252):

autofilter : bool, default False
    Whether to apply autofilter to the header row.

Is there a specific reason to remove the decorator, or would you prefer a different docstring format? If you saw something else at L2149, can you point me to the exact location?


To write a single {klass} to an Excel .xlsx file it is only necessary to
specify a target file name. To write to multiple sheets it is necessary to
create an `ExcelWriter` object with a target file name, and specify a sheet
in the file to write to.
To write a single object to an Excel .xlsx file it is only necessary
to specify a target file name.

Multiple sheets may be written to by specifying unique `sheet_name`.
With all data written to the file it is necessary to save the changes.
Note that creating an `ExcelWriter` object with a file name that already
exists will result in the contents of the existing file being erased.
.. code-block:: python

df.to_excel("path_to_file.xlsx")

To write to different sheets of the same .xlsx file it is necessary to
create an `ExcelWriter` object with a target file name,
and specify a sheet in the file to write to.

.. code-block:: python

with pd.ExcelWriter("path_to_file.xlsx") as writer:
df1.to_excel(writer, sheet_name="Sheet_name_1")
df2.to_excel(writer, sheet_name="Sheet_name_2")

When using `ExcelWriter`, note that the objects are not written until the
`ExcelWriter` object is closed.

Parameters
----------
excel_writer : path-like, file-like, or ExcelWriter object
File path or existing ExcelWriter.
excel_writer : string, path object or ExcelWriter object
File path or existing ExcelWriter
If a string is passed, a new ExcelWriter object is created.
sheet_name : str, default 'Sheet1'
Name of sheet which will contain DataFrame.
na_rep : str, default ''
Missing data representation.
float_format : str, optional
Format string for floating point numbers. For example
``float_format="%.2f"`` will format 0.1234 to 0.12.
columns : sequence or list of str, optional
Columns to write.
Missing data representation
float_format : str, default None
Format string for floating point numbers
columns : sequence, optional
Columns to write
header : bool or list of str, default True
Write out the column names. If a list of string is given it is
assumed to be aliases for the column names.
Write out the column names. If a list of string is given
it is assumed to be aliases for the column names
index : bool, default True
Write row names (index).
index_label : str or sequence, optional
Column label for index column(s) if desired. If not specified, and
Write row names (index)
index_label : str or sequence, default None
Column label for index column(s) if desired. If None is given, and
`header` and `index` are True, then the index names are used. A
sequence should be given if the DataFrame uses MultiIndex.
startrow : int, default 0
Upper left cell row to dump data frame.
Per default (0) header is written, too.
startcol : int, default 0
Upper left cell column to dump data frame.
engine : str, optional
Write engine to use, 'openpyxl' or 'xlsxwriter'. You can also set this
via the options ``io.excel.xlsx.writer`` or
``io.excel.xlsm.writer``.

merge_cells : bool or 'columns', default False
If True, write MultiIndex index and columns as merged cells.
If 'columns', merge MultiIndex column cells only.
{encoding_parameter}
Write engine to use, 'openpyxl' or 'xlsxwriter'.
Defaults to 'xlsxwriter'.
merge_cells : bool, default True
Write MultiIndex and Hierarchical Rows as merged cells.
The indices corresponding to each row will be combined and
presented as a single cell.
inf_rep : str, default 'inf'
Representation for infinity (there is no native representation for
infinity in Excel).
{verbose_parameter}
freeze_panes : tuple of int (length 2), optional
Specifies the one-based bottommost row and rightmost column that
is to be frozen.
{storage_options}
Representation for infinity (there is no native Numpy representation
for infinity in integer dtypes)
freeze_panes : tuple of int (length 2), default None
First rows to freeze panes on. Only applicable when `freeze_panes`
is passed as a tuple.
storage_options : dict, optional
Extra options that make sense for a particular storage connection,
e.g. host, port, username, password, etc., if using a URL that
requires authentication.
engine_kwargs : dict, optional
Arbitrary keyword arguments passed to excel engine.
autofilter : bool, default False
Whether to apply autofilter to the header row.

.. versionadded:: {storage_options_versionadded}
{extra_parameters}
See Also
--------
to_csv : Write DataFrame to a comma-separated values (csv) file.
read_excel : Read from an Excel file into a DataFrame.
ExcelFile : Class for parsing tabular excel files.
ExcelWriter : Class for writing DataFrame objects into excel sheets.
read_excel : Read an Excel file into a pandas DataFrame.
read_csv : Read a comma-separated values (csv) file into DataFrame.
io.formats.style.Styler.to_excel : Add styles to Excel sheet.

Notes
-----
For compatibility with :meth:`~DataFrame.to_csv`,
to_excel serializes lists and dicts to strings before writing.

Once a workbook has been saved it is not possible to write further
data without rewriting the whole workbook.

pandas will check the number of rows, columns,
and cell character count does not exceed Excel's limitations.
All other limitations must be checked by the user.
The `engine` keyword is not supported when `excel_writer` is an
existing `ExcelWriter`.

Examples
--------

Create, write to and save a workbook:

>>> df1 = pd.DataFrame(
... [["a", "b"], ["c", "d"]],
... index=["row 1", "row 2"],
... columns=["col 1", "col 2"],
... )
>>> df1.to_excel("output.xlsx") # doctest: +SKIP

To specify the sheet name:

>>> df1.to_excel("output.xlsx", sheet_name="Sheet_name_1") # doctest: +SKIP

If you wish to write to more than one sheet in the workbook, it is
necessary to specify an ExcelWriter object:

>>> df2 = df1.copy()
>>> with pd.ExcelWriter("output.xlsx") as writer: # doctest: +SKIP
... df1.to_excel(writer, sheet_name="Sheet_name_1")
... df2.to_excel(writer, sheet_name="Sheet_name_2")

ExcelWriter can also be used to append to an existing Excel file:

>>> with pd.ExcelWriter("output.xlsx", mode="a") as writer: # doctest: +SKIP
... df1.to_excel(writer, sheet_name="Sheet_name_3")

To set the library that is used to write the Excel file,
you can pass the `engine` keyword (the default engine is
automatically chosen depending on the file extension):

>>> df1.to_excel("output1.xlsx", engine="xlsxwriter") # doctest: +SKIP
>>> df = pd.DataFrame({{"A": [1, 2, 3], "B": [4, 5, 6]}})
>>> df.to_excel("pandas_simple.xlsx")
>>> df.to_excel("pandas_simple.xlsx", engine="openpyxl")
"""
if engine_kwargs is None:
engine_kwargs = {}
# Import ExcelWriter here to avoid circular import
from pandas import ExcelWriter

df = self if isinstance(self, ABCDataFrame) else self.to_frame()
if isinstance(excel_writer, ExcelWriter):
if engine is not None:
raise ValueError(
"engine should not be specified when passing an ExcelWriter"
)
engine = excel_writer.engine
else:
excel_writer = ExcelWriter(
excel_writer,
engine=engine,
engine_kwargs=engine_kwargs,
storage_options=storage_options,
)

# Import ExcelFormatter here to avoid circular import
from pandas.io.formats.excel import ExcelFormatter

formatter = ExcelFormatter(
df,
self,
na_rep=na_rep,
cols=columns,
header=header,
float_format=float_format,
columns=columns,
header=header,
index=index,
index_label=index_label,
merge_cells=merge_cells,
inf_rep=inf_rep,
)

formatter.write(
excel_writer,
sheet_name=sheet_name,
Expand All @@ -2322,8 +2311,13 @@ def to_excel(
engine=engine,
storage_options=storage_options,
engine_kwargs=engine_kwargs,
autofilter=autofilter,
)

if not isinstance(excel_writer, ExcelWriter):
# we need to close the writer if we created it
excel_writer.close()

@final
@doc(
storage_options=_shared_docs["storage_options"],
Expand Down Expand Up @@ -4851,7 +4845,6 @@ def sort_values(
ignore_index: bool = ...,
key: ValueKeyFunc = ...,
) -> Self: ...

@overload
def sort_values(
self,
Expand Down Expand Up @@ -9614,10 +9607,10 @@ def align(
1 1 2 3 4
2 6 7 8 9
>>> other
A B C D
2 10 20 30 40
3 60 70 80 90
4 600 700 800 900
A B C D E
2 10 20 30 40 NaN
3 60 70 80 90 NaN
4 600 700 800 900 NaN

Align on columns:

Expand Down Expand Up @@ -12044,7 +12037,6 @@ def last_valid_index(self) -> Hashable:
{see_also}\
{examples}
"""

_sum_prod_doc = """
{desc}

Expand Down
3 changes: 3 additions & 0 deletions pandas/io/excel/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,7 @@ def _write_cells(
startrow: int = 0,
startcol: int = 0,
freeze_panes: tuple[int, int] | None = None,
autofilter: bool = False,
) -> None:
"""
Write given formatted cells into Excel an excel sheet
Expand All @@ -1223,6 +1224,8 @@ def _write_cells(
startcol : upper left cell column to dump data frame
freeze_panes: int tuple of length 2
contains the bottom-most row and right-most column to freeze
autofilter : bool, default False
If True, apply an autofilter to the header row over the written data range.
"""
raise NotImplementedError

Expand Down
7 changes: 7 additions & 0 deletions pandas/io/excel/_odswriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,17 @@ def _write_cells(
startrow: int = 0,
startcol: int = 0,
freeze_panes: tuple[int, int] | None = None,
autofilter: bool = False,
) -> None:
"""
Write the frame cells using odf
"""
if autofilter:
raise NotImplementedError(
"Autofilter is not supported with the 'odf' engine. "
"Please use 'openpyxl' or 'xlsxwriter' engine instead."
)

from odf.table import (
Table,
TableCell,
Expand Down
31 changes: 29 additions & 2 deletions pandas/io/excel/_openpyxl.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ def _write_cells(
startrow: int = 0,
startcol: int = 0,
freeze_panes: tuple[int, int] | None = None,
autofilter: bool = False,
) -> None:
# Write the frame cells using openpyxl.
sheet_name = self._get_sheet_name(sheet_name)
Expand Down Expand Up @@ -486,6 +487,11 @@ def _write_cells(
row=freeze_panes[0] + 1, column=freeze_panes[1] + 1
)

min_r = None
min_c = None
max_r = None
max_c = None

for cell in cells:
xcell = wks.cell(
row=startrow + cell.row + 1, column=startcol + cell.col + 1
Expand All @@ -506,10 +512,23 @@ def _write_cells(
for k, v in style_kwargs.items():
setattr(xcell, k, v)

abs_row = startrow + cell.row + 1
abs_col = startcol + cell.col + 1

# track bounds (1-based for openpyxl)
if min_r is None or abs_row < min_r:
Comment on lines +518 to +519
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we able to determine these by startrow / startcol along with the DataFrame shape?

Copy link
Author

@antznette1 antznette1 Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we able to determine these by startrow / startcol along with the DataFrame shape?

@rhshadrach
I looked into calculating the autofilter bounds from startrow/startcol and DataFrame shape, but _write_cells only receives the cells list, startrow, and startcol—not the DataFrame shape, header/index flags, or MultiIndex structure.

To use shape-based calculation, we'd need to:

  • Pass additional parameters (DataFrame shape, header/index flags, MultiIndex levels) through the call chain
  • Handle edge cases like filtered columns, custom formatting that changes row/col counts, and MultiIndex headers with varying depths

The current approach tracks bounds while iterating through the cells we're already writing:

  • Matches what's actually written
  • No API changes needed
    -Minimal overhead
  • Handles edge cases (MultiIndex, merged cells, conditional formatting)

If you prefer a shape-based calculation, I can add those parameters to _write_cells, though it will increase complexity. The current tracking approach is straightforward and accurate.

min_r = abs_row
if min_c is None or abs_col < min_c:
min_c = abs_col
if max_r is None or abs_row > max_r:
max_r = abs_row
if max_c is None or abs_col > max_c:
max_c = abs_col

if cell.mergestart is not None and cell.mergeend is not None:
wks.merge_cells(
start_row=startrow + cell.row + 1,
start_column=startcol + cell.col + 1,
start_row=abs_row,
start_column=abs_col,
end_column=startcol + cell.mergeend + 1,
end_row=startrow + cell.mergestart + 1,
)
Expand All @@ -532,6 +551,14 @@ def _write_cells(
for k, v in style_kwargs.items():
setattr(xcell, k, v)

if autofilter and min_r is not None and min_c is not None and max_r is not None and max_c is not None:
# Convert numeric bounds to Excel-style range e.g. A1:D10
from openpyxl.utils import get_column_letter

start_ref = f"{get_column_letter(min_c)}{min_r}"
end_ref = f"{get_column_letter(max_c)}{max_r}"
wks.auto_filter.ref = f"{start_ref}:{end_ref}"


class OpenpyxlReader(BaseExcelReader["Workbook"]):
@doc(storage_options=_shared_docs["storage_options"])
Expand Down
Loading
Loading