Repository: pandas-dev/pandas
Pull request: ENH: add `environment`, e.g. "longtable", to `Styler.to_latex`
checks an item off #41649 

closes #37443 

adds the longtable template, similar to DataFrame.to_latex.

also allows for a custom environment if \begin{table} should be replaced by \begin{figure} etc.


Problem statement:
ENH: to_latex(): add posibility to define the latex environment (instead of table)
#### Is your feature request related to a problem?

I wish pandas's to_latex() function could create table* environment instead of table for wide tables. The generated table sometimes does not fit into one column of a two column document. 

#### Describe the solution you'd like

A parameter where one can define the name of the environment. E.g. `df.to_latex(env="foobar*")` to generate: `\begin{foobar*} table stuff here \end{foobar*}`.

#### API breaking implications

One more parameter.

#### Describe alternatives you've considered

Write a code, which replaces begin{table} and end{table} to the desired environment. (table* in most of the cases)

Hints from issue discussion:
Why not just `df.to_latex().replace("{table}","{table*}")`?  Since you aren't proposing fundamentally changing the rendering of the table, it seems like there is not a strong case for expanding the API.

> Why not just `df.to_latex().replace("{table}","{table*}")`? Since you aren't proposing fundamentally changing the rendering of the table, it seems like there is not a strong case for expanding the API.

I would be happy to use something like that, but the snippet you wrote, does not work with the latest release (1.1.3).
```
Traceback (most recent call last):
  File "soa.py", line 236, in <module>
    column_format="cccccccccccccc"
AttributeError: 'NoneType' object has no attribute 'replace'

```
for 
```
df_table.to_latex(
            buf=tex_out,
            index=False,
            caption="Comparison with the State of the Art",
            label="tab:soa",
            na_rep="---",
            escape=False,
            bold_rows=True,
            column_format="cccccccccccccc"
            ).replace("{table}","{table*}")
```
You need to not pass buf to get a string, which you can then call `replace` on.

```
buf.write(
    df_table.to_latex(
        index=False,
        caption="Comparison with the State of the Art",
        label="tab:soa",
        na_rep="---",
        escape=False,
        bold_rows=True,
        column_format="cccccccccccccc",
    ).replace("{table}", "{table*}")
)

```


I would like to have this functionality, you don't necessarily need a tabular envirionment, it could be a matrix, a pmatrix, or some other environment. Or maybe you don't want any headers at all!

Commit message: more readable (ivan request)

Patch to review:
diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst
index f532c1b8834d2..2be8a4268d882 100644
--- a/doc/source/whatsnew/v1.3.0.rst
+++ b/doc/source/whatsnew/v1.3.0.rst
@@ -136,7 +136,7 @@ which has been revised and improved (:issue:`39720`, :issue:`39317`, :issue:`404
  - Many features of the :class:`.Styler` class are now either partially or fully usable on a DataFrame with a non-unique indexes or columns (:issue:`41143`)
  - One has greater control of the display through separate sparsification of the index or columns using the :ref:`new styler options <options.available>`, which are also usable via :func:`option_context` (:issue:`41142`)
  - Added the option ``styler.render.max_elements`` to avoid browser overload when styling large DataFrames (:issue:`40712`)
- - Added the method :meth:`.Styler.to_latex` (:issue:`21673`), which also allows some limited CSS conversion (:issue:`40731`)
+ - Added the method :meth:`.Styler.to_latex` (:issue:`21673`, :issue:`41866`), which also allows some limited CSS conversion (:issue:`40731`)
  - Added the method :meth:`.Styler.to_html` (:issue:`13379`)
  - Added the method :meth:`.Styler.set_sticky` to make index and column headers permanently visible in scrolling HTML frames (:issue:`29072`)
 
diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py
index c03275b565fd4..4227e6024d417 100644
--- a/pandas/io/formats/style.py
+++ b/pandas/io/formats/style.py
@@ -428,6 +428,7 @@ def to_latex(
         multirow_align: str = "c",
         multicol_align: str = "r",
         siunitx: bool = False,
+        environment: str | None = None,
         encoding: str | None = None,
         convert_css: bool = False,
     ):
@@ -484,6 +485,11 @@ def to_latex(
             the left, centrally, or at the right.
         siunitx : bool, default False
             Set to ``True`` to structure LaTeX compatible with the {siunitx} package.
+        environment : str, optional
+            If given, the environment that will replace 'table' in ``\\begin{table}``.
+            If 'longtable' is specified then a more suitable template is
+            rendered for which the ``position_float`` argument is nullified and does not
+            impact the result.
         encoding : str, default "utf-8"
             Character encoding setting.
         convert_css : bool, default False
@@ -787,6 +793,7 @@ def to_latex(
             sparse_columns=sparse_columns,
             multirow_align=multirow_align,
             multicol_align=multicol_align,
+            environment=environment,
             convert_css=convert_css,
         )
 
diff --git a/pandas/io/formats/templates/latex.tpl b/pandas/io/formats/templates/latex.tpl
index fe081676d87af..ae341bbc29823 100644
--- a/pandas/io/formats/templates/latex.tpl
+++ b/pandas/io/formats/templates/latex.tpl
@@ -1,52 +1,5 @@
-{% if parse_wrap(table_styles, caption) %}
-\begin{table}
-{%- set position = parse_table(table_styles, 'position') %}
-{%- if position is not none %}
-[{{position}}]
-{%- endif %}
-
-{% set position_float = parse_table(table_styles, 'position_float') %}
-{% if position_float is not none%}
-\{{position_float}}
-{% endif %}
-{% if caption and caption is string %}
-\caption{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %}
-
-{% elif caption and caption is sequence %}
-\caption[{{caption[1]}}]{% raw %}{{% endraw %}{{caption[0]}}{% raw %}}{% endraw %}
-
-{% endif %}
-{% for style in table_styles %}
-{% if style['selector'] not in ['position', 'position_float', 'caption', 'toprule', 'midrule', 'bottomrule', 'column_format'] %}
-\{{style['selector']}}{{parse_table(table_styles, style['selector'])}}
-{% endif %}
-{% endfor %}
-{% endif %}
-\begin{tabular}
-{%- set column_format = parse_table(table_styles, 'column_format') %}
-{% raw %}{{% endraw %}{{column_format}}{% raw %}}{% endraw %}
-
-{% set toprule = parse_table(table_styles, 'toprule') %}
-{% if toprule is not none %}
-\{{toprule}}
-{% endif %}
-{% for row in head %}
-{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, True)}}{% endfor %} \\
-{% endfor %}
-{% set midrule = parse_table(table_styles, 'midrule') %}
-{% if midrule is not none %}
-\{{midrule}}
-{% endif %}
-{% for row in body %}
-{% for c in row %}{% if not loop.first %} & {% endif %}
-  {%- if c.type == 'th' %}{{parse_header(c, multirow_align, multicol_align)}}{% else %}{{parse_cell(c.cellstyle, c.display_value, convert_css)}}{% endif %}
-{%- endfor %} \\
-{% endfor %}
-{% set bottomrule = parse_table(table_styles, 'bottomrule') %}
-{% if bottomrule is not none %}
-\{{bottomrule}}
-{% endif %}
-\end{tabular}
-{% if parse_wrap(table_styles, caption) %}
-\end{table}
+{% if environment == "longtable" %}
+{% include "latex_longtable.tpl" %}
+{% else %}
+{% include "latex_table.tpl" %}
 {% endif %}
diff --git a/pandas/io/formats/templates/latex_longtable.tpl b/pandas/io/formats/templates/latex_longtable.tpl
new file mode 100644
index 0000000000000..fdb45aea83c16
--- /dev/null
+++ b/pandas/io/formats/templates/latex_longtable.tpl
@@ -0,0 +1,73 @@
+\begin{longtable}
+{%- set position = parse_table(table_styles, 'position') %}
+{%- if position is not none %}
+[{{position}}]
+{%- endif %}
+{%- set column_format = parse_table(table_styles, 'column_format') %}
+{% raw %}{{% endraw %}{{column_format}}{% raw %}}{% endraw %}
+
+{% for style in table_styles %}
+{% if style['selector'] not in ['position', 'position_float', 'caption', 'toprule', 'midrule', 'bottomrule', 'column_format', 'label'] %}
+\{{style['selector']}}{{parse_table(table_styles, style['selector'])}}
+{% endif %}
+{% endfor %}
+{% if caption and caption is string %}
+\caption{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %}
+{%- set label = parse_table(table_styles, 'label') %}
+{%- if label is not none %}
+ \label{{label}}
+{%- endif %} \\
+{% elif caption and caption is sequence %}
+\caption[{{caption[1]}}]{% raw %}{{% endraw %}{{caption[0]}}{% raw %}}{% endraw %}
+{%- set label = parse_table(table_styles, 'label') %}
+{%- if label is not none %}
+ \label{{label}}
+{%- endif %} \\
+{% endif %}
+{% set toprule = parse_table(table_styles, 'toprule') %}
+{% if toprule is not none %}
+\{{toprule}}
+{% endif %}
+{% for row in head %}
+{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, True)}}{% endfor %} \\
+{% endfor %}
+{% set midrule = parse_table(table_styles, 'midrule') %}
+{% if midrule is not none %}
+\{{midrule}}
+{% endif %}
+\endfirsthead
+{% if caption and caption is string %}
+\caption[]{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %} \\
+{% elif caption and caption is sequence %}
+\caption[]{% raw %}{{% endraw %}{{caption[0]}}{% raw %}}{% endraw %} \\
+{% endif %}
+{% if toprule is not none %}
+\{{toprule}}
+{% endif %}
+{% for row in head %}
+{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, True)}}{% endfor %} \\
+{% endfor %}
+{% if midrule is not none %}
+\{{midrule}}
+{% endif %}
+\endhead
+{% if midrule is not none %}
+\{{midrule}}
+{% endif %}
+\multicolumn{% raw %}{{% endraw %}{{column_format|length}}{% raw %}}{% endraw %}{r}{Continued on next page} \\
+{% if midrule is not none %}
+\{{midrule}}
+{% endif %}
+\endfoot
+{% set bottomrule = parse_table(table_styles, 'bottomrule') %}
+{% if bottomrule is not none %}
+\{{bottomrule}}
+{% endif %}
+\endlastfoot
+{% for row in body %}
+{% for c in row %}{% if not loop.first %} & {% endif %}
+  {%- if c.type == 'th' %}{{parse_header(c, multirow_align, multicol_align)}}{% else %}{{parse_cell(c.cellstyle, c.display_value, convert_css)}}{% endif %}
+{%- endfor %} \\
+{% endfor %}
+\end{longtable}
+{% raw %}{% endraw %}
diff --git a/pandas/io/formats/templates/latex_table.tpl b/pandas/io/formats/templates/latex_table.tpl
new file mode 100644
index 0000000000000..dfdd160351d02
--- /dev/null
+++ b/pandas/io/formats/templates/latex_table.tpl
@@ -0,0 +1,53 @@
+{% if environment or parse_wrap(table_styles, caption) %}
+\begin{% raw %}{{% endraw %}{{environment if environment else "table"}}{% raw %}}{% endraw %}
+{%- set position = parse_table(table_styles, 'position') %}
+{%- if position is not none %}
+[{{position}}]
+{%- endif %}
+
+{% set position_float = parse_table(table_styles, 'position_float') %}
+{% if position_float is not none%}
+\{{position_float}}
+{% endif %}
+{% if caption and caption is string %}
+\caption{% raw %}{{% endraw %}{{caption}}{% raw %}}{% endraw %}
+
+{% elif caption and caption is sequence %}
+\caption[{{caption[1]}}]{% raw %}{{% endraw %}{{caption[0]}}{% raw %}}{% endraw %}
+
+{% endif %}
+{% for style in table_styles %}
+{% if style['selector'] not in ['position', 'position_float', 'caption', 'toprule', 'midrule', 'bottomrule', 'column_format'] %}
+\{{style['selector']}}{{parse_table(table_styles, style['selector'])}}
+{% endif %}
+{% endfor %}
+{% endif %}
+\begin{tabular}
+{%- set column_format = parse_table(table_styles, 'column_format') %}
+{% raw %}{{% endraw %}{{column_format}}{% raw %}}{% endraw %}
+
+{% set toprule = parse_table(table_styles, 'toprule') %}
+{% if toprule is not none %}
+\{{toprule}}
+{% endif %}
+{% for row in head %}
+{% for c in row %}{%- if not loop.first %} & {% endif %}{{parse_header(c, multirow_align, multicol_align, True)}}{% endfor %} \\
+{% endfor %}
+{% set midrule = parse_table(table_styles, 'midrule') %}
+{% if midrule is not none %}
+\{{midrule}}
+{% endif %}
+{% for row in body %}
+{% for c in row %}{% if not loop.first %} & {% endif %}
+  {%- if c.type == 'th' %}{{parse_header(c, multirow_align, multicol_align)}}{% else %}{{parse_cell(c.cellstyle, c.display_value, convert_css)}}{% endif %}
+{%- endfor %} \\
+{% endfor %}
+{% set bottomrule = parse_table(table_styles, 'bottomrule') %}
+{% if bottomrule is not none %}
+\{{bottomrule}}
+{% endif %}
+\end{tabular}
+{% if environment or parse_wrap(table_styles, caption) %}
+\end{% raw %}{{% endraw %}{{environment if environment else "table"}}{% raw %}}{% endraw %}
+
+{% endif %}
diff --git a/pandas/tests/io/formats/style/test_to_latex.py b/pandas/tests/io/formats/style/test_to_latex.py
index 91ac652e1f652..1e88e727436bf 100644
--- a/pandas/tests/io/formats/style/test_to_latex.py
+++ b/pandas/tests/io/formats/style/test_to_latex.py
@@ -484,8 +484,131 @@ def test_parse_latex_css_conversion(css, expected):
     assert result == expected
 
 
+@pytest.mark.parametrize("environment", ["tabular", "longtable"])
+@pytest.mark.parametrize(
+    "convert, exp", [(True, "bfseries"), (False, "font-weightbold")]
+)
+def test_parse_latex_css_convert_minimal(styler, environment, convert, exp):
+    # parameters ensure longtable template is also tested
+    styler.highlight_max(props="font-weight:bold;")
+    result = styler.to_latex(convert_css=convert, environment=environment)
+    expected = dedent(
+        f"""\
+        0 & 0 & \\{exp} -0.61 & ab \\\\
+        1 & \\{exp} 1 & -1.22 & \\{exp} cd \\\\
+        \\end{{{environment}}}
+    """
+    )
+    assert expected in result
+
+
 def test_parse_latex_css_conversion_option():
     css = [("command", "option--latex--wrap")]
     expected = [("command", "option--wrap")]
     result = _parse_latex_css_conversion(css)
     assert result == expected
+
+
+def test_longtable_comprehensive(styler):
+    result = styler.to_latex(
+        environment="longtable", hrules=True, label="fig:A", caption=("full", "short")
+    )
+    expected = dedent(
+        """\
+        \\begin{longtable}{lrrl}
+        \\caption[short]{full} \\label{fig:A} \\\\
+        \\toprule
+        {} & {A} & {B} & {C} \\\\
+        \\midrule
+        \\endfirsthead
+        \\caption[]{full} \\\\
+        \\toprule
+        {} & {A} & {B} & {C} \\\\
+        \\midrule
+        \\endhead
+        \\midrule
+        \\multicolumn{4}{r}{Continued on next page} \\\\
+        \\midrule
+        \\endfoot
+        \\bottomrule
+        \\endlastfoot
+        0 & 0 & -0.61 & ab \\\\
+        1 & 1 & -1.22 & cd \\\\
+        \\end{longtable}
+    """
+    )
+    assert result == expected
+
+
+def test_longtable_minimal(styler):
+    result = styler.to_latex(environment="longtable")
+    expected = dedent(
+        """\
+        \\begin{longtable}{lrrl}
+        {} & {A} & {B} & {C} \\\\
+        \\endfirsthead
+        {} & {A} & {B} & {C} \\\\
+        \\endhead
+        \\multicolumn{4}{r}{Continued on next page} \\\\
+        \\endfoot
+        \\endlastfoot
+        0 & 0 & -0.61 & ab \\\\
+        1 & 1 & -1.22 & cd \\\\
+        \\end{longtable}
+    """
+    )
+    assert result == expected
+
+
+@pytest.mark.parametrize(
+    "sparse, exp",
+    [(True, "{} & \\multicolumn{2}{r}{A} & {B}"), (False, "{} & {A} & {A} & {B}")],
+)
+def test_longtable_multiindex_columns(df, sparse, exp):
+    cidx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
+    df.columns = cidx
+    expected = dedent(
+        f"""\
+        \\begin{{longtable}}{{lrrl}}
+        {exp} \\\\
+        {{}} & {{a}} & {{b}} & {{c}} \\\\
+        \\endfirsthead
+        {exp} \\\\
+        {{}} & {{a}} & {{b}} & {{c}} \\\\
+        \\endhead
+        """
+    )
+    assert expected in df.style.to_latex(environment="longtable", sparse_columns=sparse)
+
+
+@pytest.mark.parametrize("caption", ["full", ("full", "short")])
+@pytest.mark.parametrize("label", [None, "tab:A"])
+def test_longtable_caption_label(styler, caption, label):
+    if isinstance(caption, str):
+        cap1 = f"\\caption{{{caption}}}"
+        cap2 = f"\\caption[]{{{caption}}}"
+    elif isinstance(caption, tuple):
+        cap1 = f"\\caption[{caption[1]}]{{{caption[0]}}}"
+        cap2 = f"\\caption[]{{{caption[0]}}}"
+
+    lab = "" if label is None else f" \\label{{{label}}}"
+
+    expected = dedent(
+        f"""\
+        {cap1}{lab} \\\\
+        {{}} & {{A}} & {{B}} & {{C}} \\\\
+        \\endfirsthead
+        {cap2} \\\\
+        """
+    )
+    assert expected in styler.to_latex(
+        environment="longtable", caption=caption, label=label
+    )
+
+
+def test_latex_environment(styler):
+    result = styler.to_latex(environment="figure*")
+    assert "\\begin{table}" not in result
+    assert "\\end{table}" not in result
+    assert "\\begin{figure*}" in result
+    assert "\\end{figure*}" in result


Code review comments:
diff_hunk: @@ -484,8 +484,131 @@ def test_parse_latex_css_conversion(css, expected):
     assert result == expected
 
 
+@pytest.mark.parametrize("environment", ["tabular", "longtable"])
+@pytest.mark.parametrize(
+    "convert, exp", [(True, "bfseries"), (False, "font-weightbold")]
+)
+def test_parse_latex_css_convert_minimal(styler, environment, convert, exp):
+    # parameters ensure longtable template is also tested
+    styler.highlight_max(props="font-weight:bold;")
+    result = styler.to_latex(convert_css=convert, environment=environment)
+    expected = dedent(
+        f"""\
+        0 & 0 & \\{exp} -0.61 & ab \\\\
+        1 & \\{exp} 1 & -1.22 & \\{exp} cd \\\\
+        \\end{{{environment}}}
+    """
+    )
+    assert expected in result
+
+
 def test_parse_latex_css_conversion_option():
     css = [("command", "option--latex--wrap")]
     expected = [("command", "option--wrap")]
     result = _parse_latex_css_conversion(css)
     assert result == expected
+
+
+def test_longtable_comprehensive(styler):
+    result = styler.to_latex(
+        environment="longtable", hrules=True, label="fig:A", caption=("full", "short")
+    )
+    expected = dedent(
+        """\
+        \\begin{longtable}{lrrl}
+        \\caption[short]{full} \\label{fig:A} \\\\
+        \\toprule
+        {} & {A} & {B} & {C} \\\\
+        \\midrule
+        \\endfirsthead
+        \\caption[]{full} \\\\
+        \\toprule
+        {} & {A} & {B} & {C} \\\\
+        \\midrule
+        \\endhead
+        \\midrule
+        \\multicolumn{4}{r}{Continued on next page} \\\\
+        \\midrule
+        \\endfoot
+        \\bottomrule
+        \\endlastfoot
+        0 & 0 & -0.61 & ab \\\\
+        1 & 1 & -1.22 & cd \\\\
+        \\end{longtable}
+    """
+    )
+    assert result == expected
+
+
+def test_longtable_minimal(styler):
+    result = styler.to_latex(environment="longtable")
+    expected = dedent(
+        """\
+        \\begin{longtable}{lrrl}
+        {} & {A} & {B} & {C} \\\\
+        \\endfirsthead
+        {} & {A} & {B} & {C} \\\\
+        \\endhead
+        \\multicolumn{4}{r}{Continued on next page} \\\\
+        \\endfoot
+        \\endlastfoot
+        0 & 0 & -0.61 & ab \\\\
+        1 & 1 & -1.22 & cd \\\\
+        \\end{longtable}
+    """
+    )
+    assert result == expected
+
+
+@pytest.mark.parametrize(
+    "sparse, exp",
+    [(True, "{} & \\multicolumn{2}{r}{A} & {B}"), (False, "{} & {A} & {A} & {B}")],
path: pandas/tests/io/formats/style/test_to_latex.py
text: @user1:
It will be more readable if you split it this way:
```
[
    (True, "{} & \\multicolumn{2}{r}{A} & {B}"),
    (False, "{} & {A} & {A} & {B}"),
],
```

@author:
done

@author:
actually failed black pandas, and black modified back to the original. so couldnt'y make this change.

@user1:
If you leave a trailing comma on the second line, then, I believe, black will be fine with that and would not change it.

@author:
ok done.

diff_hunk: @@ -484,8 +484,131 @@ def test_parse_latex_css_conversion(css, expected):
     assert result == expected
 
 
+@pytest.mark.parametrize("environment", ["tabular", "longtable"])
+@pytest.mark.parametrize(
+    "convert, exp", [(True, "bfseries"), (False, "font-weightbold")]
+)
+def test_parse_latex_css_convert_minimal(styler, environment, convert, exp):
+    # parameters ensure longtable template is also tested
+    styler.highlight_max(props="font-weight:bold;")
+    result = styler.to_latex(convert_css=convert, environment=environment)
+    expected = dedent(
+        f"""\
+        0 & 0 & \\{exp} -0.61 & ab \\\\
+        1 & \\{exp} 1 & -1.22 & \\{exp} cd \\\\
+        \\end{{{environment}}}
+    """
+    )
+    assert expected in result
+
+
 def test_parse_latex_css_conversion_option():
     css = [("command", "option--latex--wrap")]
     expected = [("command", "option--wrap")]
     result = _parse_latex_css_conversion(css)
     assert result == expected
+
+
+def test_longtable_comprehensive(styler):
+    result = styler.to_latex(
+        environment="longtable", hrules=True, label="fig:A", caption=("full", "short")
+    )
+    expected = dedent(
+        """\
+        \\begin{longtable}{lrrl}
+        \\caption[short]{full} \\label{fig:A} \\\\
+        \\toprule
+        {} & {A} & {B} & {C} \\\\
+        \\midrule
+        \\endfirsthead
+        \\caption[]{full} \\\\
+        \\toprule
+        {} & {A} & {B} & {C} \\\\
+        \\midrule
+        \\endhead
+        \\midrule
+        \\multicolumn{4}{r}{Continued on next page} \\\\
+        \\midrule
+        \\endfoot
+        \\bottomrule
+        \\endlastfoot
+        0 & 0 & -0.61 & ab \\\\
+        1 & 1 & -1.22 & cd \\\\
+        \\end{longtable}
+    """
+    )
+    assert result == expected
+
+
+def test_longtable_minimal(styler):
+    result = styler.to_latex(environment="longtable")
+    expected = dedent(
+        """\
+        \\begin{longtable}{lrrl}
+        {} & {A} & {B} & {C} \\\\
+        \\endfirsthead
+        {} & {A} & {B} & {C} \\\\
+        \\endhead
+        \\multicolumn{4}{r}{Continued on next page} \\\\
+        \\endfoot
+        \\endlastfoot
+        0 & 0 & -0.61 & ab \\\\
+        1 & 1 & -1.22 & cd \\\\
+        \\end{longtable}
+    """
+    )
+    assert result == expected
+
+
+@pytest.mark.parametrize(
+    "sparse, exp",
+    [(True, "{} & \\multicolumn{2}{r}{A} & {B}"), (False, "{} & {A} & {A} & {B}")],
+)
+def test_longtable_multiindex_columns(df, sparse, exp):
+    cidx = MultiIndex.from_tuples([("A", "a"), ("A", "b"), ("B", "c")])
+    df.columns = cidx
+    expected = dedent(
+        f"""\
+        \\begin{{longtable}}{{lrrl}}
+        {exp} \\\\
+        {{}} & {{a}} & {{b}} & {{c}} \\\\
+        \\endfirsthead
+        {exp} \\\\
+        {{}} & {{a}} & {{b}} & {{c}} \\\\
+        \\endhead
+        """
+    )
+    assert expected in df.style.to_latex(environment="longtable", sparse_columns=sparse)
+
+
+@pytest.mark.parametrize("caption", ["full", ("full", "short")])
+@pytest.mark.parametrize("label", [None, "tab:A"])
+def test_longtable_caption_label(styler, caption, label):
+    if isinstance(caption, str):
path: pandas/tests/io/formats/style/test_to_latex.py
text: @user1:
I suggest against conditional logic in tests.
If parametrization makes this test so complex, then better not to parametrize.

@author:
I do in general agree with your philosophy, although in this case as you observed the logic is an extension to the parametrization, and the test is isomorphic to one with:
```
@user2.mark.parametrize("caption, cap1, cap2", [(...), (...)])
@user2.mark.parametrize("label, lab", [(...), (...])
def test_longtable_caption_label(styler, caption, cap1, cap2, label, lab): 
# no conditional logic.
```
I don't see this as a blocker, maybe just artistic difference! I also take your point about complexity but think this test is still pretty low level in that too.


@user1:
You could put the expected macros right here to avoid any conditional logic and make tests more readable:
```
@user2.mark.parametrize(
    "caption, caption_macro",
    [
        ("full", "\caption{full}"),
        (("full", "short"), "\caption[short]{full}"),
    ],
)
@user2.mark.parametrize(
    "label, label_macro",
    [
        (None, ""),
        ("tab:A", "\label{tab:A}"),
    ],
)
```

This way, will not need branching inside the test.

@user1:
But in your case there will be cap1 and cap2, rather than a single caption macro.

@author:
I believe this is more readable the way I previously structured it. Never-the-less I have changed it to you preference which is also fine with me.