[{"data":1,"prerenderedAt":2427},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-latex-table":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-latex-table":2391,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-latex-table":2400,"children:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-latex-table":2405,"related:\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-latex-table":2406},{"id":4,"title":5,"body":6,"dateModified":2358,"datePublished":2358,"description":2359,"extension":2360,"faq":2361,"keyword":2370,"kind":2371,"meta":2372,"navigation":123,"path":2380,"seo":2381,"slug":2384,"stem":2385,"topics":2386,"type":2389,"__hash__":2390},"docs\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-latex-table\u002Findex.md","Convert an Excel Sheet to a LaTeX Table with Python",{"type":7,"value":8,"toc":2343},"minimark",[9,28,33,64,91,95,186,210,343,347,358,403,415,419,432,588,618,825,850,1063,1067,1074,1164,1189,1193,1203,1442,1456,1595,1599,1602,1810,1830,1834,1837,2168,2183,2187,2245,2249,2255,2259,2278,2282,2288,2294,2300,2306,2310,2339],[10,11,12,13,17,18,21,22,27],"p",{},"Results that live in a workbook often end up in a paper, a thesis, a technical report or a PDF built from LaTeX. Retyping a table is slow and error-prone, and online converters choke on anything beyond a small grid. pandas writes LaTeX directly — ",[14,15,16],"code",{},"DataFrame.to_latex"," and the more flexible ",[14,19,20],{},"Styler.to_latex"," — so the table can be regenerated whenever the workbook changes. The catch is that LaTeX treats several ordinary characters as commands: an ampersand in a region name splits the row into an extra column, an underscore in a product code stops compilation, and a percent sign silently comments out the rest of the line. This page shows the conversion, the escaping that prevents those failures, and a compile check that proves the output works. It belongs to ",[23,24,26],"a",{"href":25},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002F","Handling Excel File Formats and Conversions",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl jinja2\n","bash","",[14,41,42],{"__ignoreMap":39},[43,44,47,51,55,58,61],"span",{"class":45,"line":46},"line",1,[43,48,50],{"class":49},"sMTad","pip",[43,52,54],{"class":53},"srMev"," install",[43,56,57],{"class":53}," pandas",[43,59,60],{"class":53}," openpyxl",[43,62,63],{"class":53}," jinja2\n",[10,65,66,67,70,71,74,75,78,79,82,83,86,87,90],{},"pandas 2 and later build LaTeX through the Styler, which needs ",[14,68,69],{},"jinja2",". A LaTeX installation (",[14,72,73],{},"pdflatex",") is only needed for the compile check in Step 6. ",[14,76,77],{},"results.xlsx"," has a ",[14,80,81],{},"Summary"," sheet whose table starts on row 3, below a title, with a region called ",[14,84,85],{},"North & East"," and one called ",[14,88,89],{},"West_2"," — two names that break naive conversions.",[29,92,94],{"id":93},"step-1-read-the-range-and-look-at-the-default-output","Step 1 — Read the range and look at the default output",[34,96,100],{"className":97,"code":98,"language":99,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","import pandas as pd\n\ndf = pd.read_excel(\"results.xlsx\", sheet_name=\"Summary\", header=2)\nprint(df.to_latex(index=False))\n","python",[14,101,102,118,125,166],{"__ignoreMap":39},[43,103,104,108,112,115],{"class":45,"line":46},[43,105,107],{"class":106},"s-kum","import",[43,109,111],{"class":110},"skGVy"," pandas ",[43,113,114],{"class":106},"as",[43,116,117],{"class":110}," pd\n",[43,119,121],{"class":45,"line":120},2,[43,122,124],{"emptyLinePlaceholder":123},true,"\n",[43,126,128,131,134,137,140,143,147,149,152,154,157,159,163],{"class":45,"line":127},3,[43,129,130],{"class":110},"df ",[43,132,133],{"class":106},"=",[43,135,136],{"class":110}," pd.read_excel(",[43,138,139],{"class":53},"\"results.xlsx\"",[43,141,142],{"class":110},", ",[43,144,146],{"class":145},"sa561","sheet_name",[43,148,133],{"class":106},[43,150,151],{"class":53},"\"Summary\"",[43,153,142],{"class":110},[43,155,156],{"class":145},"header",[43,158,133],{"class":106},[43,160,162],{"class":161},"sP0c6","2",[43,164,165],{"class":110},")\n",[43,167,169,172,175,178,180,183],{"class":45,"line":168},4,[43,170,171],{"class":161},"print",[43,173,174],{"class":110},"(df.to_latex(",[43,176,177],{"class":145},"index",[43,179,133],{"class":106},[43,181,182],{"class":161},"False",[43,184,185],{"class":110},"))\n",[10,187,188,191,192,196,197,200,201,203,204,206,207,209],{},[14,189,190],{},"header=2"," skips the title rows, as described in ",[23,193,195],{"href":194},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fskip-rows-and-set-header-when-reading-excel-with-pandas\u002F","Skip Rows and Set the Header When Reading Excel with pandas",". The default output is a ",[14,198,199],{},"tabular"," with booktabs rules and six decimal places on every number. It also contains ",[14,202,85],{}," and ",[14,205,89],{}," exactly as typed: ",[14,208,16],{}," does not escape by default, so the ampersand becomes a column separator — the row now has five cells for four columns — and the underscore triggers \"Missing $ inserted\" when the document compiles.",[211,212,220,221,220,225,220,229,220,237,220,245,220,252,220,255,220,259,220,262,220,265,220,269,220,273,220,277,220,281,220,286,220,290,220,294,220,298,220,300,220,303,220,305,220,308,220,313,220,317,220,324,220,329,220,337],"svg",{"viewBox":213,"role":214,"ariaLabelledBy":215,"xmlns":218,"style":219},"141 0 477 140","img",[216,217],"lx-escape-t","lx-escape-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:477px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[222,223,224],"title",{"id":216},"Unescaped text changes the table's structure",[226,227,228],"desc",{"id":217},"The Excel row North & East with revenue 1234567.89 becomes, without escaping, a LaTeX row where the ampersand is read as a column separator and the region splits into two cells. With escaping the ampersand is written as a backslash ampersand and stays text.",[230,231],"rect",{"x":232,"y":233,"width":234,"height":235,"fill":236},"141","0","477","140","#ffffff",[230,238],{"x":239,"y":240,"width":241,"height":242,"rx":233,"fill":236,"stroke":243,"style":244},"157.9","40.0","93.7","28.0","var(--line,#cdd5e6)","stroke-width:1px",[246,247,251],"text",{"x":248,"y":249,"style":250},"204.8","59.0","font-size:11px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Region",[230,253],{"x":239,"y":254,"width":241,"height":242,"rx":233,"fill":236,"stroke":243,"style":244},"68.0",[246,256,85],{"x":248,"y":257,"style":258},"87.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle",[230,260],{"x":239,"y":261,"width":241,"height":242,"rx":233,"fill":236,"stroke":243,"style":244},"96.0",[246,263,89],{"x":248,"y":264,"style":258},"115.0",[230,266],{"x":267,"y":240,"width":268,"height":242,"rx":233,"fill":236,"stroke":243,"style":244},"389.3","104.2",[246,270,272],{"x":271,"y":249,"style":250},"441.4","escape off",[230,274],{"x":275,"y":240,"width":276,"height":242,"rx":233,"fill":236,"stroke":243,"style":244},"493.4","108.6",[246,278,280],{"x":279,"y":249,"style":250},"547.8","escape on",[230,282],{"x":267,"y":254,"width":268,"height":242,"rx":233,"fill":283,"stroke":284,"style":285},"#fce9e9","var(--danger,#dc2626)","stroke-width:1.5px",[246,287,289],{"x":271,"y":257,"style":288},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","North & East &",[230,291],{"x":275,"y":254,"width":276,"height":242,"rx":233,"fill":292,"stroke":293,"style":285},"#d9f4f1","var(--teal,#0f9488)",[246,295,297],{"x":279,"y":257,"style":296},"font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","North \\& East &",[230,299],{"x":267,"y":261,"width":268,"height":242,"rx":233,"fill":283,"stroke":284,"style":285},[246,301,302],{"x":271,"y":264,"style":288},"West_2 &",[230,304],{"x":275,"y":261,"width":276,"height":242,"rx":233,"fill":292,"stroke":293,"style":285},[246,306,307],{"x":279,"y":264,"style":296},"West\\_2 &",[246,309,312],{"x":248,"y":310,"style":311},"29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","Excel cell",[246,314,316],{"x":315,"y":310,"style":311},"495.7","LaTeX row (escape off \u002F on)",[45,318],{"x1":319,"y1":320,"x2":321,"y2":320,"stroke":322,"style":323},"261.6","82.0","375.3","var(--brand,#5b5cf0)","stroke-width:2px",[325,326],"polygon",{"points":327,"fill":328},"379.3,82.0 370.3,87.0 370.3,77.0","#5b5cf0",[230,330],{"x":331,"y":332,"width":333,"height":334,"rx":335,"fill":236,"stroke":243,"style":336},"281.4","49.0","78.2","22.0","11","stroke-width:1.2px",[246,338,342],{"x":339,"y":340,"style":341},"320.4","64.0","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","to_latex",[29,344,346],{"id":345},"step-2-escape-special-characters","Step 2 — Escape special characters",[10,348,349,350,353,354,357],{},"LaTeX reserves ten characters: ",[14,351,352],{},"& % $ # _ { } ~ ^ \\",". Passing ",[14,355,356],{},"escape=True"," replaces each with its safe form, in the data and in the column headers:",[34,359,361],{"className":97,"code":360,"language":99,"meta":39,"style":39},"print(df.to_latex(index=False, escape=True, float_format=\"%.2f\"))\n",[14,362,363],{"__ignoreMap":39},[43,364,365,367,369,371,373,375,377,380,382,385,387,390,392,395,399,401],{"class":45,"line":46},[43,366,171],{"class":161},[43,368,174],{"class":110},[43,370,177],{"class":145},[43,372,133],{"class":106},[43,374,182],{"class":161},[43,376,142],{"class":110},[43,378,379],{"class":145},"escape",[43,381,133],{"class":106},[43,383,384],{"class":161},"True",[43,386,142],{"class":110},[43,388,389],{"class":145},"float_format",[43,391,133],{"class":106},[43,393,394],{"class":53},"\"",[43,396,398],{"class":397},"sSjpA","%.2f",[43,400,394],{"class":53},[43,402,185],{"class":110},[10,404,405,203,408,411,412,414],{},[14,406,407],{},"North \\& East",[14,409,410],{},"West\\_2"," now print as text, and ",[14,413,389],{}," cuts the noise to two decimals. For a quick table that is enough. Reports usually want different formats per column — thousands separators on revenue, percentages for growth and margin — and that is where the Styler comes in.",[29,416,418],{"id":417},"step-3-format-each-column-with-the-styler","Step 3 — Format each column with the Styler",[10,420,421,424,425,427,428,431],{},[14,422,423],{},"df.style.format"," takes a format per column and an ",[14,426,379],{}," option; ",[14,429,430],{},"format_index"," does the same for the header row:",[34,433,435],{"className":97,"code":434,"language":99,"meta":39,"style":39},"styled = (\n    df.style.hide(axis=\"index\")\n    .format({\"Revenue\": \"{:,.0f}\", \"Growth\": \"{:+.1%}\", \"Margin\": \"{:.1%}\"}, escape=\"latex\")\n    .format_index(escape=\"latex\", axis=1)\n)\nprint(styled.to_latex(hrules=True, column_format=\"lrrr\"))\n",[14,436,437,447,462,534,556,561],{"__ignoreMap":39},[43,438,439,442,444],{"class":45,"line":46},[43,440,441],{"class":110},"styled ",[43,443,133],{"class":106},[43,445,446],{"class":110}," (\n",[43,448,449,452,455,457,460],{"class":45,"line":120},[43,450,451],{"class":110},"    df.style.hide(",[43,453,454],{"class":145},"axis",[43,456,133],{"class":106},[43,458,459],{"class":53},"\"index\"",[43,461,165],{"class":110},[43,463,464,467,470,473,475,478,481,484,486,488,491,493,495,497,500,502,504,506,509,511,513,515,518,520,522,525,527,529,532],{"class":45,"line":127},[43,465,466],{"class":110},"    .format({",[43,468,469],{"class":53},"\"Revenue\"",[43,471,472],{"class":110},": ",[43,474,394],{"class":53},[43,476,477],{"class":397},"{",[43,479,480],{"class":106},":,.0f",[43,482,483],{"class":397},"}",[43,485,394],{"class":53},[43,487,142],{"class":110},[43,489,490],{"class":53},"\"Growth\"",[43,492,472],{"class":110},[43,494,394],{"class":53},[43,496,477],{"class":397},[43,498,499],{"class":106},":+.1%",[43,501,483],{"class":397},[43,503,394],{"class":53},[43,505,142],{"class":110},[43,507,508],{"class":53},"\"Margin\"",[43,510,472],{"class":110},[43,512,394],{"class":53},[43,514,477],{"class":397},[43,516,517],{"class":106},":.1%",[43,519,483],{"class":397},[43,521,394],{"class":53},[43,523,524],{"class":110},"}, ",[43,526,379],{"class":145},[43,528,133],{"class":106},[43,530,531],{"class":53},"\"latex\"",[43,533,165],{"class":110},[43,535,536,539,541,543,545,547,549,551,554],{"class":45,"line":168},[43,537,538],{"class":110},"    .format_index(",[43,540,379],{"class":145},[43,542,133],{"class":106},[43,544,531],{"class":53},[43,546,142],{"class":110},[43,548,454],{"class":145},[43,550,133],{"class":106},[43,552,553],{"class":161},"1",[43,555,165],{"class":110},[43,557,559],{"class":45,"line":558},5,[43,560,165],{"class":110},[43,562,564,566,569,572,574,576,578,581,583,586],{"class":45,"line":563},6,[43,565,171],{"class":161},[43,567,568],{"class":110},"(styled.to_latex(",[43,570,571],{"class":145},"hrules",[43,573,133],{"class":106},[43,575,384],{"class":161},[43,577,142],{"class":110},[43,579,580],{"class":145},"column_format",[43,582,133],{"class":106},[43,584,585],{"class":53},"\"lrrr\"",[43,587,185],{"class":110},[10,589,590,591,594,595,597,598,602,603,606,607,610,611,613,614,617],{},"Look closely at the output: the region names are escaped, but the percentages read ",[14,592,593],{},"+12.5%",". The ",[14,596,379],{}," option escapes the ",[599,600,601],"em",{},"cell value"," before formatting, not the text the format string produces, so the ",[14,604,605],{},"%"," added by ",[14,608,609],{},"{:.1%}"," goes through raw — and in LaTeX ",[14,612,605],{}," starts a comment, so everything after it on that line, including the ",[14,615,616],{},"\\\\"," that ends the row, disappears. The table does not fail loudly; it runs rows together. Produce percentages with an escaped sign instead:",[34,619,621],{"className":97,"code":620,"language":99,"meta":39,"style":39},"pct = lambda v: f\"{v * 100:+.1f}\\\\%\"\nstyled = (\n    df.style.hide(axis=\"index\")\n    .format({\"Revenue\": \"{:,.0f}\", \"Growth\": pct, \"Margin\": lambda v: f\"{v * 100:.1f}\\\\%\"})\n    .format(escape=\"latex\", subset=[\"Region\"])\n    .format_index(escape=\"latex\", axis=1)\n)\nbody = styled.to_latex(hrules=True, column_format=\"lrrr\")\nprint(body)\n",[14,622,623,661,669,681,738,765,785,790,817],{"__ignoreMap":39},[43,624,625,628,630,633,636,639,641,643,646,649,652,655,658],{"class":45,"line":46},[43,626,627],{"class":110},"pct ",[43,629,133],{"class":106},[43,631,632],{"class":106}," lambda",[43,634,635],{"class":110}," v: ",[43,637,638],{"class":106},"f",[43,640,394],{"class":53},[43,642,477],{"class":397},[43,644,645],{"class":110},"v ",[43,647,648],{"class":106},"*",[43,650,651],{"class":161}," 100",[43,653,654],{"class":106},":+.1f",[43,656,657],{"class":397},"}\\\\",[43,659,660],{"class":53},"%\"\n",[43,662,663,665,667],{"class":45,"line":120},[43,664,441],{"class":110},[43,666,133],{"class":106},[43,668,446],{"class":110},[43,670,671,673,675,677,679],{"class":45,"line":127},[43,672,451],{"class":110},[43,674,454],{"class":145},[43,676,133],{"class":106},[43,678,459],{"class":53},[43,680,165],{"class":110},[43,682,683,685,687,689,691,693,695,697,699,701,703,706,708,710,713,715,717,719,721,723,725,727,730,732,735],{"class":45,"line":168},[43,684,466],{"class":110},[43,686,469],{"class":53},[43,688,472],{"class":110},[43,690,394],{"class":53},[43,692,477],{"class":397},[43,694,480],{"class":106},[43,696,483],{"class":397},[43,698,394],{"class":53},[43,700,142],{"class":110},[43,702,490],{"class":53},[43,704,705],{"class":110},": pct, ",[43,707,508],{"class":53},[43,709,472],{"class":110},[43,711,712],{"class":106},"lambda",[43,714,635],{"class":110},[43,716,638],{"class":106},[43,718,394],{"class":53},[43,720,477],{"class":397},[43,722,645],{"class":110},[43,724,648],{"class":106},[43,726,651],{"class":161},[43,728,729],{"class":106},":.1f",[43,731,657],{"class":397},[43,733,734],{"class":53},"%\"",[43,736,737],{"class":110},"})\n",[43,739,740,743,745,747,749,751,754,756,759,762],{"class":45,"line":558},[43,741,742],{"class":110},"    .format(",[43,744,379],{"class":145},[43,746,133],{"class":106},[43,748,531],{"class":53},[43,750,142],{"class":110},[43,752,753],{"class":145},"subset",[43,755,133],{"class":106},[43,757,758],{"class":110},"[",[43,760,761],{"class":53},"\"Region\"",[43,763,764],{"class":110},"])\n",[43,766,767,769,771,773,775,777,779,781,783],{"class":45,"line":563},[43,768,538],{"class":110},[43,770,379],{"class":145},[43,772,133],{"class":106},[43,774,531],{"class":53},[43,776,142],{"class":110},[43,778,454],{"class":145},[43,780,133],{"class":106},[43,782,553],{"class":161},[43,784,165],{"class":110},[43,786,788],{"class":45,"line":787},7,[43,789,165],{"class":110},[43,791,793,796,798,801,803,805,807,809,811,813,815],{"class":45,"line":792},8,[43,794,795],{"class":110},"body ",[43,797,133],{"class":106},[43,799,800],{"class":110}," styled.to_latex(",[43,802,571],{"class":145},[43,804,133],{"class":106},[43,806,384],{"class":161},[43,808,142],{"class":110},[43,810,580],{"class":145},[43,812,133],{"class":106},[43,814,585],{"class":53},[43,816,165],{"class":110},[43,818,820,822],{"class":45,"line":819},9,[43,821,171],{"class":161},[43,823,824],{"class":110},"(body)\n",[10,826,827,828,831,832,835,836,142,839,203,842,845,846,849],{},"Callables give complete control over the text: each one returns a string with ",[14,829,830],{},"\\%",", which LaTeX prints as a percent sign. Escaping is applied only to the text column, so the backslashes the callables add are not escaped a second time. ",[14,833,834],{},"hrules=True"," adds the booktabs ",[14,837,838],{},"\\toprule",[14,840,841],{},"\\midrule",[14,843,844],{},"\\bottomrule","; ",[14,847,848],{},"column_format=\"lrrr\""," left-aligns the text column and right-aligns the numbers so the digits line up.",[211,851,220,857,220,860,220,863,220,868,220,876,220,881,220,890,220,893,220,966,220,968,220,972,220,974,220,1036,220,1041,220,1044,220,1047,220,1051,220,1054,220,1057,220,1059],{"viewBox":852,"role":214,"ariaLabelledBy":853,"xmlns":218,"style":856},"4 0 752 156",[854,855],"lx-pct-t","lx-pct-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[222,858,859],{"id":854},"Why a percentage format can swallow the end of a row",[226,861,862],{"id":855},"A LaTeX table row produced with the format {:.1%}. The percent sign after 12.5 starts a comment, so the row terminator and everything after it on the line are ignored. Writing backslash percent keeps the row intact.",[230,864],{"x":865,"y":233,"width":866,"height":867,"fill":236},"4","752","156",[230,869],{"x":870,"y":871,"width":872,"height":873,"rx":874,"fill":875,"stroke":243,"style":285},"20.0","16.0","720.0","66.0","12","#f0f2f5",[246,877,553],{"x":878,"y":879,"style":880},"60.0","47.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",[882,883],"circle",{"cx":884,"cy":885,"r":886,"fill":887,"stroke":888,"style":889},"36.0","42.5","11.0","#b91c1c","none","stroke-width:0px",[246,891,553],{"x":884,"y":879,"style":892},"font-size:12px;font-weight:700;fill:#ffffff;text-anchor:middle",[246,894,896,901,904,907,910,912,915,917,919,921,924,927,930,932,935,937,939,941,944,947,949,951,953,955,958,960,962,964],{"x":320,"y":879,"style":895},"font-size:12.5px;text-anchor:start;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",[897,898,900],"tspan",{"style":899},"fill:var(--text,#172033);font-weight:400","North",[897,902,903],{"style":899}," ",[897,905,906],{"style":899},"\\",[897,908,909],{"style":899},"&",[897,911,903],{"style":899},[897,913,914],{"style":899},"East",[897,916,903],{"style":899},[897,918,909],{"style":899},[897,920,903],{"style":899},[897,922,553],{"style":923},"fill:var(--gold-ink,#7a4e06);font-weight:400",[897,925,926],{"style":899},",",[897,928,929],{"style":923},"234",[897,931,926],{"style":899},[897,933,934],{"style":923},"568",[897,936,903],{"style":899},[897,938,909],{"style":899},[897,940,903],{"style":899},[897,942,943],{"style":899},"+",[897,945,946],{"style":923},"12.5",[897,948,605],{"style":899},[897,950,903],{"style":899},[897,952,909],{"style":899},[897,954,903],{"style":899},[897,956,957],{"style":923},"21.4",[897,959,605],{"style":899},[897,961,903],{"style":899},[897,963,906],{"style":899},[897,965,906],{"style":899},[246,967,162],{"x":878,"y":254,"style":880},[882,969],{"cx":884,"cy":970,"r":886,"fill":971,"stroke":888,"style":889},"63.5","#0b6157",[246,973,162],{"x":884,"y":254,"style":892},[246,975,976,978,980,982,984,986,988,990,992,994,996,998,1000,1002,1004,1006,1008,1010,1012,1014,1016,1018,1020,1022,1024,1026,1028,1030,1032,1034],{"x":320,"y":254,"style":895},[897,977,900],{"style":899},[897,979,903],{"style":899},[897,981,906],{"style":899},[897,983,909],{"style":899},[897,985,903],{"style":899},[897,987,914],{"style":899},[897,989,903],{"style":899},[897,991,909],{"style":899},[897,993,903],{"style":899},[897,995,553],{"style":923},[897,997,926],{"style":899},[897,999,929],{"style":923},[897,1001,926],{"style":899},[897,1003,934],{"style":923},[897,1005,903],{"style":899},[897,1007,909],{"style":899},[897,1009,903],{"style":899},[897,1011,943],{"style":899},[897,1013,946],{"style":923},[897,1015,906],{"style":899},[897,1017,605],{"style":899},[897,1019,903],{"style":899},[897,1021,909],{"style":899},[897,1023,903],{"style":899},[897,1025,957],{"style":923},[897,1027,906],{"style":899},[897,1029,605],{"style":899},[897,1031,903],{"style":899},[897,1033,906],{"style":899},[897,1035,906],{"style":899},[230,1037],{"x":870,"y":261,"width":1038,"height":1039,"rx":1040,"fill":283,"stroke":284,"style":285},"353.0","44.0","10",[882,1042],{"cx":240,"cy":1043,"r":886,"fill":887,"stroke":888,"style":889},"118.0",[246,1045,553],{"x":240,"y":1046,"style":892},"122.5",[246,1048,1050],{"x":878,"y":1046,"style":1049},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","% starts a comment: & 21.4% \\\\ is ignored",[230,1052],{"x":1053,"y":261,"width":1038,"height":1039,"rx":1040,"fill":292,"stroke":293,"style":285},"387.0",[882,1055],{"cx":1056,"cy":1043,"r":886,"fill":971,"stroke":888,"style":889},"407.0",[246,1058,162],{"x":1056,"y":1046,"style":892},[246,1060,1062],{"x":1061,"y":1046,"style":1049},"427.0","\\% prints a percent sign and the row ends",[29,1064,1066],{"id":1065},"step-4-wrap-it-in-a-table-with-a-caption-and-a-label","Step 4 — Wrap it in a table with a caption and a label",[10,1068,1069,1070,1073],{},"A float environment gives the table a number, a caption and a label for ",[14,1071,1072],{},"\\ref{}",":",[34,1075,1077],{"className":97,"code":1076,"language":99,"meta":39,"style":39},"table = styled.to_latex(\n    hrules=True, column_format=\"lrrr\",\n    caption=\"Revenue, growth and margin by region, FY2026\",\n    label=\"tab:regions\", position=\"htbp\", position_float=\"centering\",\n)\nprint(table)\n",[14,1078,1079,1089,1109,1121,1153,1157],{"__ignoreMap":39},[43,1080,1081,1084,1086],{"class":45,"line":46},[43,1082,1083],{"class":110},"table ",[43,1085,133],{"class":106},[43,1087,1088],{"class":110}," styled.to_latex(\n",[43,1090,1091,1094,1096,1098,1100,1102,1104,1106],{"class":45,"line":120},[43,1092,1093],{"class":145},"    hrules",[43,1095,133],{"class":106},[43,1097,384],{"class":161},[43,1099,142],{"class":110},[43,1101,580],{"class":145},[43,1103,133],{"class":106},[43,1105,585],{"class":53},[43,1107,1108],{"class":110},",\n",[43,1110,1111,1114,1116,1119],{"class":45,"line":127},[43,1112,1113],{"class":145},"    caption",[43,1115,133],{"class":106},[43,1117,1118],{"class":53},"\"Revenue, growth and margin by region, FY2026\"",[43,1120,1108],{"class":110},[43,1122,1123,1126,1128,1131,1133,1136,1138,1141,1143,1146,1148,1151],{"class":45,"line":168},[43,1124,1125],{"class":145},"    label",[43,1127,133],{"class":106},[43,1129,1130],{"class":53},"\"tab:regions\"",[43,1132,142],{"class":110},[43,1134,1135],{"class":145},"position",[43,1137,133],{"class":106},[43,1139,1140],{"class":53},"\"htbp\"",[43,1142,142],{"class":110},[43,1144,1145],{"class":145},"position_float",[43,1147,133],{"class":106},[43,1149,1150],{"class":53},"\"centering\"",[43,1152,1108],{"class":110},[43,1154,1155],{"class":45,"line":558},[43,1156,165],{"class":110},[43,1158,1159,1161],{"class":45,"line":563},[43,1160,171],{"class":161},[43,1162,1163],{"class":110},"(table)\n",[10,1165,1166,1167,1170,1171,142,1174,203,1177,1180,1181,1184,1185,1188],{},"The output is a complete ",[14,1168,1169],{},"table"," environment with ",[14,1172,1173],{},"\\centering",[14,1175,1176],{},"\\caption{…}",[14,1178,1179],{},"\\label{tab:regions}","; drop it into a document with ",[14,1182,1183],{},"\\input{regions.tex}",". Writing the table to its own file and inputting it means the document picks up new numbers whenever the script reruns, without anyone editing the ",[14,1186,1187],{},".tex"," source.",[29,1190,1192],{"id":1191},"step-5-long-tables-that-span-pages","Step 5 — Long tables that span pages",[10,1194,1195,1196,1198,1199,1202],{},"A ",[14,1197,1169],{}," float cannot break across pages, so a 200-row listing runs off the bottom. Use the ",[14,1200,1201],{},"longtable"," environment, which repeats the header on every page:",[34,1204,1206],{"className":97,"code":1205,"language":99,"meta":39,"style":39},"rows = pd.concat([df] * 30, ignore_index=True)\nlong_table = (\n    rows.style.hide(axis=\"index\")\n    .format({\"Revenue\": \"{:,.0f}\", \"Growth\": pct, \"Margin\": lambda v: f\"{v * 100:.1f}\\\\%\"})\n    .format(escape=\"latex\", subset=[\"Region\"])\n    .format_index(escape=\"latex\", axis=1)\n    .to_latex(environment=\"longtable\", hrules=True, column_format=\"lrrr\",\n              caption=\"All regional rows\", label=\"tab:long\")\n)\nprint(long_table.count(r\"\\\\\"), \"row terminators;\", \"endhead\" in long_table)\n",[14,1207,1208,1234,1243,1256,1308,1330,1350,1381,1403,1407],{"__ignoreMap":39},[43,1209,1210,1213,1215,1218,1220,1223,1225,1228,1230,1232],{"class":45,"line":46},[43,1211,1212],{"class":110},"rows ",[43,1214,133],{"class":106},[43,1216,1217],{"class":110}," pd.concat([df] ",[43,1219,648],{"class":106},[43,1221,1222],{"class":161}," 30",[43,1224,142],{"class":110},[43,1226,1227],{"class":145},"ignore_index",[43,1229,133],{"class":106},[43,1231,384],{"class":161},[43,1233,165],{"class":110},[43,1235,1236,1239,1241],{"class":45,"line":120},[43,1237,1238],{"class":110},"long_table ",[43,1240,133],{"class":106},[43,1242,446],{"class":110},[43,1244,1245,1248,1250,1252,1254],{"class":45,"line":127},[43,1246,1247],{"class":110},"    rows.style.hide(",[43,1249,454],{"class":145},[43,1251,133],{"class":106},[43,1253,459],{"class":53},[43,1255,165],{"class":110},[43,1257,1258,1260,1262,1264,1266,1268,1270,1272,1274,1276,1278,1280,1282,1284,1286,1288,1290,1292,1294,1296,1298,1300,1302,1304,1306],{"class":45,"line":168},[43,1259,466],{"class":110},[43,1261,469],{"class":53},[43,1263,472],{"class":110},[43,1265,394],{"class":53},[43,1267,477],{"class":397},[43,1269,480],{"class":106},[43,1271,483],{"class":397},[43,1273,394],{"class":53},[43,1275,142],{"class":110},[43,1277,490],{"class":53},[43,1279,705],{"class":110},[43,1281,508],{"class":53},[43,1283,472],{"class":110},[43,1285,712],{"class":106},[43,1287,635],{"class":110},[43,1289,638],{"class":106},[43,1291,394],{"class":53},[43,1293,477],{"class":397},[43,1295,645],{"class":110},[43,1297,648],{"class":106},[43,1299,651],{"class":161},[43,1301,729],{"class":106},[43,1303,657],{"class":397},[43,1305,734],{"class":53},[43,1307,737],{"class":110},[43,1309,1310,1312,1314,1316,1318,1320,1322,1324,1326,1328],{"class":45,"line":558},[43,1311,742],{"class":110},[43,1313,379],{"class":145},[43,1315,133],{"class":106},[43,1317,531],{"class":53},[43,1319,142],{"class":110},[43,1321,753],{"class":145},[43,1323,133],{"class":106},[43,1325,758],{"class":110},[43,1327,761],{"class":53},[43,1329,764],{"class":110},[43,1331,1332,1334,1336,1338,1340,1342,1344,1346,1348],{"class":45,"line":563},[43,1333,538],{"class":110},[43,1335,379],{"class":145},[43,1337,133],{"class":106},[43,1339,531],{"class":53},[43,1341,142],{"class":110},[43,1343,454],{"class":145},[43,1345,133],{"class":106},[43,1347,553],{"class":161},[43,1349,165],{"class":110},[43,1351,1352,1355,1358,1360,1363,1365,1367,1369,1371,1373,1375,1377,1379],{"class":45,"line":787},[43,1353,1354],{"class":110},"    .to_latex(",[43,1356,1357],{"class":145},"environment",[43,1359,133],{"class":106},[43,1361,1362],{"class":53},"\"longtable\"",[43,1364,142],{"class":110},[43,1366,571],{"class":145},[43,1368,133],{"class":106},[43,1370,384],{"class":161},[43,1372,142],{"class":110},[43,1374,580],{"class":145},[43,1376,133],{"class":106},[43,1378,585],{"class":53},[43,1380,1108],{"class":110},[43,1382,1383,1386,1388,1391,1393,1396,1398,1401],{"class":45,"line":792},[43,1384,1385],{"class":145},"              caption",[43,1387,133],{"class":106},[43,1389,1390],{"class":53},"\"All regional rows\"",[43,1392,142],{"class":110},[43,1394,1395],{"class":145},"label",[43,1397,133],{"class":106},[43,1399,1400],{"class":53},"\"tab:long\"",[43,1402,165],{"class":110},[43,1404,1405],{"class":45,"line":819},[43,1406,165],{"class":110},[43,1408,1410,1412,1415,1418,1420,1423,1425,1428,1431,1433,1436,1439],{"class":45,"line":1409},10,[43,1411,171],{"class":161},[43,1413,1414],{"class":110},"(long_table.count(",[43,1416,1417],{"class":106},"r",[43,1419,394],{"class":53},[43,1421,616],{"class":1422},"s_b0D",[43,1424,394],{"class":53},[43,1426,1427],{"class":110},"), ",[43,1429,1430],{"class":53},"\"row terminators;\"",[43,1432,142],{"class":110},[43,1434,1435],{"class":53},"\"endhead\"",[43,1437,1438],{"class":106}," in",[43,1440,1441],{"class":110}," long_table)\n",[10,1443,1444,1445,1448,1449,1452,1453,27],{},"The output includes ",[14,1446,1447],{},"\\endhead",", which tells LaTeX to repeat the header block at the top of each page. It requires ",[14,1450,1451],{},"\\usepackage{longtable}"," in the preamble; booktabs rules need ",[14,1454,1455],{},"\\usepackage{booktabs}",[211,1457,220,1462,220,1465,220,1468,220,1471,220,1477,220,1487,220,1492,220,1496,220,1500,220,1503,220,1506,220,1512,220,1517,220,1522,220,1529,220,1534,220,1539,220,1541,220,1544,220,1548,220,1551,220,1554,220,1560,220,1562,220,1565,220,1567,220,1570,220,1574,220,1577,220,1581,220,1588,220,1592],{"viewBox":1458,"role":214,"ariaLabelledBy":1459,"xmlns":218,"style":856},"4 0 752 272",[1460,1461],"lx-env-t","lx-env-d",[222,1463,1464],{"id":1460},"Which LaTeX environment to generate",[226,1466,1467],{"id":1461},"Short tables that fit on a page go into a table float with a caption and label. Long listings use longtable so the header repeats. Tables embedded in running text or a minipage use a bare tabular.",[230,1469],{"x":865,"y":233,"width":866,"height":1470,"fill":236},"272",[230,1472],{"x":1473,"y":871,"width":1474,"height":1475,"rx":874,"fill":1476,"stroke":322,"style":323},"240.0","280.0","75.5","#f0f4ff",[1478,1479,1481],"g",{"transform":1480},"translate(368.0,29.0) scale(1.000)",[1482,1483],"path",{"d":1484,"fill":888,"stroke":1485,"style":1486},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[246,1488,1491],{"x":1489,"y":1475,"style":1490},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","How will the table be used?",[45,1493],{"x1":1489,"y1":1494,"x2":1489,"y2":1495,"stroke":322,"style":323},"91.5","111.5",[45,1497],{"x1":1498,"y1":1495,"x2":1499,"y2":1495,"stroke":322,"style":323},"134.7","625.3",[45,1501],{"x1":1498,"y1":1495,"x2":1498,"y2":1502,"stroke":322,"style":323},"157.5",[325,1504],{"points":1505,"fill":328},"134.7,161.5 129.7,152.5 139.7,152.5",[230,1507],{"x":1508,"y":1509,"width":1510,"height":1511,"rx":1040,"fill":236,"stroke":243,"style":336},"86.0","121.5","97.2","21.0",[246,1513,1516],{"x":1498,"y":1514,"style":1515},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","fits on a page",[230,1518],{"x":870,"y":1519,"width":1520,"height":1521,"rx":874,"fill":292,"stroke":293,"style":323},"163.5","229.3","93.0",[1478,1523,1525],{"transform":1524},"translate(122.7,176.5) scale(1.000)",[1482,1526],{"d":1527,"fill":888,"stroke":1528,"style":1486},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--teal-ink,#0b6157)",[246,1530,1533],{"x":1498,"y":1531,"style":1532},"223.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","table float",[246,1535,1538],{"x":1498,"y":1536,"style":1537},"242.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","caption, label, position=htbp",[45,1540],{"x1":1489,"y1":1495,"x2":1489,"y2":1502,"stroke":322,"style":323},[325,1542],{"points":1543,"fill":328},"380.0,161.5 375.0,152.5 385.0,152.5",[230,1545],{"x":1546,"y":1509,"width":1547,"height":1511,"rx":1040,"fill":236,"stroke":243,"style":336},"338.5","83.0",[246,1549,1550],{"x":1489,"y":1514,"style":1515},"many rows",[230,1552],{"x":1553,"y":1519,"width":1520,"height":1521,"rx":874,"fill":1476,"stroke":322,"style":323},"265.3",[1478,1555,1557],{"transform":1556},"translate(368.0,176.5) scale(1.000)",[1482,1558],{"d":1559,"fill":888,"stroke":1485,"style":1486},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5",[246,1561,1201],{"x":1489,"y":1531,"style":1490},[246,1563,1564],{"x":1489,"y":1536,"style":1537},"header repeats via \\endhead",[45,1566],{"x1":1499,"y1":1495,"x2":1499,"y2":1502,"stroke":322,"style":323},[325,1568],{"points":1569,"fill":328},"625.3,161.5 620.3,152.5 630.3,152.5",[230,1571],{"x":1572,"y":1509,"width":1573,"height":1511,"rx":1040,"fill":236,"stroke":243,"style":336},"557.1","136.4",[246,1575,1576],{"x":1499,"y":1514,"style":1515},"inside other content",[230,1578],{"x":1579,"y":1519,"width":1520,"height":1521,"rx":874,"fill":1580,"stroke":243,"style":323},"510.7","#e7ebef",[1478,1582,1584],{"transform":1583},"translate(613.3,176.5) scale(1.000)",[1482,1585],{"d":1586,"fill":888,"stroke":1587,"style":1486},"M9 8l-4 4 4 4 M15 8l4 4-4 4","var(--text,#172033)",[246,1589,1591],{"x":1499,"y":1531,"style":1590},"font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","bare tabular",[246,1593,1594],{"x":1499,"y":1536,"style":1537},"default to_latex output",[29,1596,1598],{"id":1597},"step-6-prove-that-it-compiles","Step 6 — Prove that it compiles",[10,1600,1601],{},"The cheapest test is to compile a minimal document around the table. A failed compile names the line, which points straight at the unescaped character:",[34,1603,1605],{"className":97,"code":1604,"language":99,"meta":39,"style":39},"from pathlib import Path\nimport subprocess\n\nPath(\"regions.tex\").write_text(table)\nPath(\"check.tex\").write_text(\n    \"\\\\documentclass{article}\\n\\\\usepackage{booktabs}\\n\\\\usepackage{longtable}\\n\"\n    \"\\\\begin{document}\\n\\\\input{regions.tex}\\n\\\\end{document}\\n\"\n)\nresult = subprocess.run([\"pdflatex\", \"-interaction=nonstopmode\", \"-halt-on-error\", \"check.tex\"],\n                        capture_output=True, text=True)\nprint(\"compiled\" if result.returncode == 0 else result.stdout[-800:])\nprint(Path(\"check.pdf\").exists())\n",[14,1606,1607,1620,1627,1631,1642,1652,1679,1705,1709,1739,1758,1796],{"__ignoreMap":39},[43,1608,1609,1612,1615,1617],{"class":45,"line":46},[43,1610,1611],{"class":106},"from",[43,1613,1614],{"class":110}," pathlib ",[43,1616,107],{"class":106},[43,1618,1619],{"class":110}," Path\n",[43,1621,1622,1624],{"class":45,"line":120},[43,1623,107],{"class":106},[43,1625,1626],{"class":110}," subprocess\n",[43,1628,1629],{"class":45,"line":127},[43,1630,124],{"emptyLinePlaceholder":123},[43,1632,1633,1636,1639],{"class":45,"line":168},[43,1634,1635],{"class":110},"Path(",[43,1637,1638],{"class":53},"\"regions.tex\"",[43,1640,1641],{"class":110},").write_text(table)\n",[43,1643,1644,1646,1649],{"class":45,"line":558},[43,1645,1635],{"class":110},[43,1647,1648],{"class":53},"\"check.tex\"",[43,1650,1651],{"class":110},").write_text(\n",[43,1653,1654,1657,1659,1662,1665,1668,1671,1673,1676],{"class":45,"line":563},[43,1655,1656],{"class":53},"    \"",[43,1658,616],{"class":397},[43,1660,1661],{"class":53},"documentclass",[43,1663,1664],{"class":397},"{article}\\n\\\\",[43,1666,1667],{"class":53},"usepackage",[43,1669,1670],{"class":397},"{booktabs}\\n\\\\",[43,1672,1667],{"class":53},[43,1674,1675],{"class":397},"{longtable}\\n",[43,1677,1678],{"class":53},"\"\n",[43,1680,1681,1683,1685,1688,1691,1694,1697,1700,1703],{"class":45,"line":787},[43,1682,1656],{"class":53},[43,1684,616],{"class":397},[43,1686,1687],{"class":53},"begin",[43,1689,1690],{"class":397},"{document}\\n\\\\",[43,1692,1693],{"class":53},"input",[43,1695,1696],{"class":397},"{regions.tex}\\n\\\\",[43,1698,1699],{"class":53},"end",[43,1701,1702],{"class":397},"{document}\\n",[43,1704,1678],{"class":53},[43,1706,1707],{"class":45,"line":792},[43,1708,165],{"class":110},[43,1710,1711,1714,1716,1719,1722,1724,1727,1729,1732,1734,1736],{"class":45,"line":819},[43,1712,1713],{"class":110},"result ",[43,1715,133],{"class":106},[43,1717,1718],{"class":110}," subprocess.run([",[43,1720,1721],{"class":53},"\"pdflatex\"",[43,1723,142],{"class":110},[43,1725,1726],{"class":53},"\"-interaction=nonstopmode\"",[43,1728,142],{"class":110},[43,1730,1731],{"class":53},"\"-halt-on-error\"",[43,1733,142],{"class":110},[43,1735,1648],{"class":53},[43,1737,1738],{"class":110},"],\n",[43,1740,1741,1744,1746,1748,1750,1752,1754,1756],{"class":45,"line":1409},[43,1742,1743],{"class":145},"                        capture_output",[43,1745,133],{"class":106},[43,1747,384],{"class":161},[43,1749,142],{"class":110},[43,1751,246],{"class":145},[43,1753,133],{"class":106},[43,1755,384],{"class":161},[43,1757,165],{"class":110},[43,1759,1761,1763,1766,1769,1772,1775,1778,1781,1784,1787,1790,1793],{"class":45,"line":1760},11,[43,1762,171],{"class":161},[43,1764,1765],{"class":110},"(",[43,1767,1768],{"class":53},"\"compiled\"",[43,1770,1771],{"class":106}," if",[43,1773,1774],{"class":110}," result.returncode ",[43,1776,1777],{"class":106},"==",[43,1779,1780],{"class":161}," 0",[43,1782,1783],{"class":106}," else",[43,1785,1786],{"class":110}," result.stdout[",[43,1788,1789],{"class":106},"-",[43,1791,1792],{"class":161},"800",[43,1794,1795],{"class":110},":])\n",[43,1797,1799,1801,1804,1807],{"class":45,"line":1798},12,[43,1800,171],{"class":161},[43,1802,1803],{"class":110},"(Path(",[43,1805,1806],{"class":53},"\"check.pdf\"",[43,1808,1809],{"class":110},").exists())\n",[10,1811,1812,1815,1816,1818,1819,1822,1823,1825,1826,27],{},[14,1813,1814],{},"-halt-on-error"," makes ",[14,1817,73],{}," stop at the first problem instead of guessing, and the return code tells the script whether the table is valid. Running the same check on the unescaped output from Step 1 stops at the North & East row with ",[14,1820,1821],{},"! Extra alignment tab has been changed to \\cr."," — LaTeX found one more ",[14,1824,909],{}," than the column specification allows. Wire this into the report's tests, like the output checks in ",[23,1827,1829],{"href":1828},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest",[29,1831,1833],{"id":1832},"step-7-regenerate-every-table-from-the-workbook","Step 7 — Regenerate every table from the workbook",[10,1835,1836],{},"Once one table works, generate all of them in a single pass so the document never shows stale numbers. Keep a small mapping from sheet to output file and caption, and write each table next to the document source:",[34,1838,1840],{"className":97,"code":1839,"language":99,"meta":39,"style":39},"specs = {\n    \"Summary\": (\"tab-regions.tex\", \"Revenue, growth and margin by region\", \"tab:regions\"),\n}\nout_dir = Path(\"tables\")\nout_dir.mkdir(exist_ok=True)\nfor sheet, (fname, caption, label) in specs.items():\n    data = pd.read_excel(\"results.xlsx\", sheet_name=sheet, header=2)\n    tex = (\n        data.style.hide(axis=\"index\")\n        .format({\"Revenue\": \"{:,.0f}\", \"Growth\": pct, \"Margin\": lambda v: f\"{v * 100:.1f}\\\\%\"})\n        .format(escape=\"latex\", subset=[\"Region\"])\n        .format_index(escape=\"latex\", axis=1)\n        .to_latex(hrules=True, column_format=\"lrrr\", caption=caption, label=label,\n                  position=\"htbp\", position_float=\"centering\")\n    )\n    (out_dir \u002F fname).write_text(tex)\nprint(sorted(p.name for p in out_dir.iterdir()))\n",[14,1841,1842,1852,1875,1880,1895,1909,1923,1951,1960,1973,2026,2049,2070,2107,2127,2133,2145],{"__ignoreMap":39},[43,1843,1844,1847,1849],{"class":45,"line":46},[43,1845,1846],{"class":110},"specs ",[43,1848,133],{"class":106},[43,1850,1851],{"class":110}," {\n",[43,1853,1854,1857,1860,1863,1865,1868,1870,1872],{"class":45,"line":120},[43,1855,1856],{"class":53},"    \"Summary\"",[43,1858,1859],{"class":110},": (",[43,1861,1862],{"class":53},"\"tab-regions.tex\"",[43,1864,142],{"class":110},[43,1866,1867],{"class":53},"\"Revenue, growth and margin by region\"",[43,1869,142],{"class":110},[43,1871,1130],{"class":53},[43,1873,1874],{"class":110},"),\n",[43,1876,1877],{"class":45,"line":127},[43,1878,1879],{"class":110},"}\n",[43,1881,1882,1885,1887,1890,1893],{"class":45,"line":168},[43,1883,1884],{"class":110},"out_dir ",[43,1886,133],{"class":106},[43,1888,1889],{"class":110}," Path(",[43,1891,1892],{"class":53},"\"tables\"",[43,1894,165],{"class":110},[43,1896,1897,1900,1903,1905,1907],{"class":45,"line":558},[43,1898,1899],{"class":110},"out_dir.mkdir(",[43,1901,1902],{"class":145},"exist_ok",[43,1904,133],{"class":106},[43,1906,384],{"class":161},[43,1908,165],{"class":110},[43,1910,1911,1914,1917,1920],{"class":45,"line":563},[43,1912,1913],{"class":106},"for",[43,1915,1916],{"class":110}," sheet, (fname, caption, label) ",[43,1918,1919],{"class":106},"in",[43,1921,1922],{"class":110}," specs.items():\n",[43,1924,1925,1928,1930,1932,1934,1936,1938,1940,1943,1945,1947,1949],{"class":45,"line":787},[43,1926,1927],{"class":110},"    data ",[43,1929,133],{"class":106},[43,1931,136],{"class":110},[43,1933,139],{"class":53},[43,1935,142],{"class":110},[43,1937,146],{"class":145},[43,1939,133],{"class":106},[43,1941,1942],{"class":110},"sheet, ",[43,1944,156],{"class":145},[43,1946,133],{"class":106},[43,1948,162],{"class":161},[43,1950,165],{"class":110},[43,1952,1953,1956,1958],{"class":45,"line":792},[43,1954,1955],{"class":110},"    tex ",[43,1957,133],{"class":106},[43,1959,446],{"class":110},[43,1961,1962,1965,1967,1969,1971],{"class":45,"line":819},[43,1963,1964],{"class":110},"        data.style.hide(",[43,1966,454],{"class":145},[43,1968,133],{"class":106},[43,1970,459],{"class":53},[43,1972,165],{"class":110},[43,1974,1975,1978,1980,1982,1984,1986,1988,1990,1992,1994,1996,1998,2000,2002,2004,2006,2008,2010,2012,2014,2016,2018,2020,2022,2024],{"class":45,"line":1409},[43,1976,1977],{"class":110},"        .format({",[43,1979,469],{"class":53},[43,1981,472],{"class":110},[43,1983,394],{"class":53},[43,1985,477],{"class":397},[43,1987,480],{"class":106},[43,1989,483],{"class":397},[43,1991,394],{"class":53},[43,1993,142],{"class":110},[43,1995,490],{"class":53},[43,1997,705],{"class":110},[43,1999,508],{"class":53},[43,2001,472],{"class":110},[43,2003,712],{"class":106},[43,2005,635],{"class":110},[43,2007,638],{"class":106},[43,2009,394],{"class":53},[43,2011,477],{"class":397},[43,2013,645],{"class":110},[43,2015,648],{"class":106},[43,2017,651],{"class":161},[43,2019,729],{"class":106},[43,2021,657],{"class":397},[43,2023,734],{"class":53},[43,2025,737],{"class":110},[43,2027,2028,2031,2033,2035,2037,2039,2041,2043,2045,2047],{"class":45,"line":1760},[43,2029,2030],{"class":110},"        .format(",[43,2032,379],{"class":145},[43,2034,133],{"class":106},[43,2036,531],{"class":53},[43,2038,142],{"class":110},[43,2040,753],{"class":145},[43,2042,133],{"class":106},[43,2044,758],{"class":110},[43,2046,761],{"class":53},[43,2048,764],{"class":110},[43,2050,2051,2054,2056,2058,2060,2062,2064,2066,2068],{"class":45,"line":1798},[43,2052,2053],{"class":110},"        .format_index(",[43,2055,379],{"class":145},[43,2057,133],{"class":106},[43,2059,531],{"class":53},[43,2061,142],{"class":110},[43,2063,454],{"class":145},[43,2065,133],{"class":106},[43,2067,553],{"class":161},[43,2069,165],{"class":110},[43,2071,2073,2076,2078,2080,2082,2084,2086,2088,2090,2092,2095,2097,2100,2102,2104],{"class":45,"line":2072},13,[43,2074,2075],{"class":110},"        .to_latex(",[43,2077,571],{"class":145},[43,2079,133],{"class":106},[43,2081,384],{"class":161},[43,2083,142],{"class":110},[43,2085,580],{"class":145},[43,2087,133],{"class":106},[43,2089,585],{"class":53},[43,2091,142],{"class":110},[43,2093,2094],{"class":145},"caption",[43,2096,133],{"class":106},[43,2098,2099],{"class":110},"caption, ",[43,2101,1395],{"class":145},[43,2103,133],{"class":106},[43,2105,2106],{"class":110},"label,\n",[43,2108,2110,2113,2115,2117,2119,2121,2123,2125],{"class":45,"line":2109},14,[43,2111,2112],{"class":145},"                  position",[43,2114,133],{"class":106},[43,2116,1140],{"class":53},[43,2118,142],{"class":110},[43,2120,1145],{"class":145},[43,2122,133],{"class":106},[43,2124,1150],{"class":53},[43,2126,165],{"class":110},[43,2128,2130],{"class":45,"line":2129},15,[43,2131,2132],{"class":110},"    )\n",[43,2134,2136,2139,2142],{"class":45,"line":2135},16,[43,2137,2138],{"class":110},"    (out_dir ",[43,2140,2141],{"class":106},"\u002F",[43,2143,2144],{"class":110}," fname).write_text(tex)\n",[43,2146,2148,2150,2152,2155,2158,2160,2163,2165],{"class":45,"line":2147},17,[43,2149,171],{"class":161},[43,2151,1765],{"class":110},[43,2153,2154],{"class":161},"sorted",[43,2156,2157],{"class":110},"(p.name ",[43,2159,1913],{"class":106},[43,2161,2162],{"class":110}," p ",[43,2164,1919],{"class":106},[43,2166,2167],{"class":110}," out_dir.iterdir()))\n",[10,2169,2170,2171,2174,2175,2177,2178,2182],{},"The document then uses ",[14,2172,2173],{},"\\input{tables\u002Ftab-regions.tex}",", and a build step — a Makefile rule, a CI job or simply running the script before ",[14,2176,73],{}," — refreshes every table from the current workbook. Because the formats live in the script, a new quarter's numbers come out with the same separators, signs and alignment as the last. If the workbook is produced by another report job, chain the two so the tables are rebuilt whenever the data is, in the same way ",[23,2179,2181],{"href":2180},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-word-documents-from-excel-rows\u002F","Generate Word Documents from Excel Rows"," feeds a document template.",[29,2184,2186],{"id":2185},"common-pitfalls","Common pitfalls",[2188,2189,2190,2206,2217,2227,2239],"ul",{},[2191,2192,2193,903,2197,2199,2200,2202,2203,27],"li",{},[2194,2195,2196],"strong",{},"Unescaped text.",[14,2198,16],{}," does not escape by default; pass ",[14,2201,356],{}," or use the Styler with ",[14,2204,2205],{},"escape=\"latex\"",[2191,2207,2208,903,2211,2213,2214,2216],{},[2194,2209,2210],{},"Percent signs from format strings.",[14,2212,609],{}," output is not escaped and comments out the rest of the row; return ",[14,2215,830],{}," from a callable.",[2191,2218,2219,2222,2223,2226],{},[2194,2220,2221],{},"Double escaping."," Escaping a column whose formatter already added backslashes produces ",[14,2224,2225],{},"\\textbackslash{}","; escape only the text columns.",[2191,2228,2229,2232,2233,2236,2237,27],{},[2194,2230,2231],{},"Missing packages."," Booktabs rules need ",[14,2234,2235],{},"booktabs","; long tables need ",[14,2238,1201],{},[2191,2240,2241,2244],{},[2194,2242,2243],{},"Index columns."," Hide the DataFrame index unless it carries meaning, or the first column will be 0, 1, 2…",[29,2246,2248],{"id":2247},"performance-notes","Performance notes",[10,2250,2251,2252,2254],{},"Generating LaTeX is fast — the Styler renders a few thousand rows in well under a second — so the time goes into reading the workbook and compiling the document. Tables of several hundred rows are rarely what a reader wants in a PDF anyway; summarise them first with a pivot, and put the full listing in an appendix built with ",[14,2253,1201],{},", or attach the workbook instead.",[29,2256,2258],{"id":2257},"conclusion","Conclusion",[10,2260,2261,2262,2264,2265,2267,2268,2270,2271,2273,2274,2277],{},"Read the range with pandas, then generate LaTeX with the Styler: format each column, escape text columns and headers with ",[14,2263,2205],{},", return percentages with an escaped ",[14,2266,830],{},", add booktabs rules, and wrap the table in a float with a caption and label — or in ",[14,2269,1201],{}," for long listings. Write it to its own ",[14,2272,1187],{}," file, ",[14,2275,2276],{},"\\input"," it, and compile a minimal document in a test so a stray ampersand never reaches the final PDF.",[29,2279,2281],{"id":2280},"frequently-asked-questions","Frequently asked questions",[10,2283,2284,2287],{},[2194,2285,2286],{},"How do I convert an Excel table to LaTeX in Python?","\nRead the sheet with pd.read_excel, then call df.to_latex(index=False, escape=True) for a simple tabular, or the Styler (df.style.format with to_latex) for per-column number formats, captions, labels and booktabs rules.",[10,2289,2290,2293],{},[2194,2291,2292],{},"Why does my LaTeX table have an extra column?","\nA cell contains an unescaped ampersand, which LaTeX reads as a column separator. Escape special characters with escape=True in to_latex or escape=\"latex\" in Styler.format.",[10,2295,2296,2299],{},[2194,2297,2298],{},"Why do rows run together after formatting percentages?","\nA format such as {:.1%} adds a bare %, which starts a LaTeX comment and hides the row terminator. Format percentages with a function that returns an escaped % instead.",[10,2301,2302,2305],{},[2194,2303,2304],{},"How do I make a LaTeX table from pandas span several pages?","\nPass environment=\"longtable\" to the Styler to_latex method, which repeats the header on every page through \\endhead, and load the longtable package in the document preamble.",[29,2307,2309],{"id":2308},"related","Related",[2188,2311,2312,2318,2325,2332],{},[2191,2313,2314,2315,2317],{},"Up one level: ",[23,2316,26],{"href":25}," — converting workbooks to and from other formats.",[2191,2319,2320,2324],{},[23,2321,2323],{"href":2322},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-markdown-table\u002F","Convert an Excel Sheet to a Markdown Table"," — the lightweight cousin for docs and READMEs.",[2191,2326,2327,2331],{},[23,2328,2330],{"href":2329},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-an-html-table\u002F","Convert an Excel Sheet to an HTML Table with Python"," — the same Styler, different output.",[2191,2333,2334,2338],{},[23,2335,2337],{"href":2336},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fexport-a-styled-pandas-dataframe-to-excel\u002F","Export a Styled pandas DataFrame to Excel"," — the Styler going the other way.",[2340,2341,2342],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}",{"title":39,"searchDepth":120,"depth":120,"links":2344},[2345,2346,2347,2348,2349,2350,2351,2352,2353,2354,2355,2356,2357],{"id":31,"depth":120,"text":32},{"id":93,"depth":120,"text":94},{"id":345,"depth":120,"text":346},{"id":417,"depth":120,"text":418},{"id":1065,"depth":120,"text":1066},{"id":1191,"depth":120,"text":1192},{"id":1597,"depth":120,"text":1598},{"id":1832,"depth":120,"text":1833},{"id":2185,"depth":120,"text":2186},{"id":2247,"depth":120,"text":2248},{"id":2257,"depth":120,"text":2258},{"id":2280,"depth":120,"text":2281},{"id":2308,"depth":120,"text":2309},"2026-10-02","Turn a range from a workbook into a LaTeX table that compiles first time: read it with pandas, write it with to_latex or the Styler, escape the characters that break LaTeX (& % _ $ #), format numbers and percentages without letting a bare % comment out the row, add booktabs rules, a caption and a label, split long tables across pages, and check the result with pdflatex.","md",[2362,2364,2366,2368],{"q":2286,"a":2363},"Read the sheet with pd.read_excel, then call df.to_latex(index=False, escape=True) for a simple tabular, or the Styler (df.style.format with to_latex) for per-column number formats, captions, labels and booktabs rules.",{"q":2292,"a":2365},"A cell contains an unescaped ampersand, which LaTeX reads as a column separator. Escape special characters with escape=True in to_latex or escape=\"latex\" in Styler.format.",{"q":2298,"a":2367},"A format such as {:.1%} adds a bare %, which starts a LaTeX comment and hides the row terminator. Format percentages with a function that returns an escaped \\% instead.",{"q":2304,"a":2369},"Pass environment=\"longtable\" to the Styler to_latex method, which repeats the header on every page through \\endhead, and load the longtable package in the document preamble.","convert an excel sheet to a latex table",null,{"breadcrumb":2373},[2374,2376,2379],{"name":2375,"item":2141},"Home",{"name":2377,"item":2378},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":26,"item":25},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-latex-table",{"title":2382,"description":2383},"Convert an Excel Sheet to a LaTeX Table with pandas","Excel to LaTeX with pandas: read the range, to_latex vs Styler.to_latex, escaping & % _ in data and headers, number and percent formats that compile, booktabs, caption and label, longtable for many rows, and a pdflatex check.","convert-an-excel-sheet-to-a-latex-table","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-latex-table\u002Findex",[2387,2388],"file-formats","pandas","how-to","qk0IB-KuVBYo7MX2Fa2Xo4jS11SCo3YZXa6M09BqcBM",[2392,2396],{"title":2393,"path":2394,"stem":2395,"children":-1},"Automate LibreOffice Calc from Python with UNO","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fautomate-libreoffice-calc-from-python-with-uno","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fautomate-libreoffice-calc-from-python-with-uno\u002Findex",{"title":2397,"path":2398,"stem":2399,"children":-1},"Convert an Excel Sheet to a Markdown Table with Python","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-markdown-table","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-a-markdown-table\u002Findex",[2401,2403],{"path":2402,"title":26},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions",{"path":2404,"title":2377},"\u002Fgetting-started-with-python-excel-automation",[],[2407,2410,2413,2417,2420,2424],{"path":2408,"title":2409,"kind":2389},"\u002Fadvanced-data-transformation-and-cleaning\u002Freading-excel-with-polars-and-arrow\u002Fconvert-excel-files-to-parquet-with-python","Convert Excel Files to Parquet with Python",{"path":2411,"title":2412,"kind":2389},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fconvert-excel-to-csv-with-python","Convert Excel to CSV with Python",{"path":2414,"title":2415,"kind":2416},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fexcel-vs-csv-vs-parquet-for-python-data-pipelines","Excel vs CSV vs Parquet for Python Data Pipelines","compare",{"path":2418,"title":2419,"kind":2416},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods","Pick an Excel Engine for .xlsx, .xlsm, .xls, .xlsb and .ods",{"path":2421,"title":2422,"kind":2423},"\u002Fadvanced-data-transformation-and-cleaning\u002Fcleaning-excel-data-with-pandas\u002Ffix-garbled-characters-in-excel-data","Fix Garbled Characters Like \"CafÃ©\" and \"â€™\" in Excel Data","fix",{"path":2425,"title":2426,"kind":2389},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fextract-vba-macro-code-from-an-excel-file","Extract VBA Macro Code from an Excel File",1790972059029]