[{"data":1,"prerenderedAt":1612},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates":3,"surround:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates":1604},{"id":4,"title":5,"body":6,"description":1595,"extension":1596,"meta":1597,"navigation":153,"path":1598,"seo":1599,"stem":1602,"__hash__":1603},"docs\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Findex.md","Generating Excel Reports from Templates",{"type":7,"value":8,"toc":1581},"minimark",[9,19,24,32,39,70,73,77,80,105,112,622,626,629,796,802,806,809,1187,1194,1198,1216,1229,1237,1250,1317,1324,1335,1368,1382,1473,1481,1485,1499,1512,1518,1522,1531,1535,1538,1577],[10,11,12,13,18],"p",{},"Inside the ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002F","Automating Reporting Workflows"," pillar, the generate stage has two strategies. You can build a workbook from scratch every run, or you can take a workbook someone in your organization already designed — logo, brand colors, formulas, print areas, a tuned page layout — and inject only the numbers that change. This cluster is about the second strategy: template injection. It is faster to write, produces output stakeholders recognize, and keeps the formatting decisions where they belong — with the person who made the template, not in your Python.",[20,21,23],"h2",{"id":22},"why-template-injection-beats-building-from-scratch","Why template injection beats building from scratch",[10,25,26,27,31],{},"When a finance team hands you a quarterly report template, that file encodes a lot of work: merged title banners, a corporate color palette, conditional formatting rules, a ",[28,29,30],"code",{},"SUM"," over the data range, a frozen header, and a print area that fits one page. Rebuilding all of that in code is brittle — every styling tweak the team makes later has to be re-translated into Python, and the two copies drift.",[10,33,34,35,38],{},"Template injection inverts the relationship. The ",[28,36,37],{},".xlsx"," is the source of truth for appearance; your script is responsible only for data. The core pattern is three lines of intent:",[40,41,42,58,64],"ol",{},[43,44,45,49,50,53,54,57],"li",{},[46,47,48],"strong",{},"Load"," the prepared template with ",[28,51,52],{},"openpyxl","'s ",[28,55,56],{},"load_workbook()",".",[43,59,60,63],{},[46,61,62],{},"Write"," values into the specific cells that hold data, leaving everything else untouched.",[43,65,66,69],{},[46,67,68],{},"Save"," the result as a new, dated file — never overwriting the template.",[10,71,72],{},"Everything below is a runnable variation on that loop. The first block builds a small styled template so each example runs end to end without you supplying a file.",[20,74,76],{"id":75},"build-a-reusable-template-in-code","Build a reusable template in code",[10,78,79],{},"In production the template comes from a designer. Here we generate one so the rest of the page is self-contained. Install the library first:",[81,82,87],"pre",{"className":83,"code":84,"language":85,"meta":86,"style":86},"language-bash shiki shiki-themes github-light github-dark","pip install openpyxl\n","bash","",[28,88,89],{"__ignoreMap":86},[90,91,94,98,102],"span",{"class":92,"line":93},"line",1,[90,95,97],{"class":96},"sScJk","pip",[90,99,101],{"class":100},"sZZnC"," install",[90,103,104],{"class":100}," openpyxl\n",[10,106,107,108,111],{},"This block writes ",[28,109,110],{},"report_template.xlsx"," with a title cell, a styled header row, a formula in the totals cell, and placeholder data cells:",[81,113,117],{"className":114,"code":115,"language":116,"meta":86,"style":86},"language-python shiki shiki-themes github-light github-dark","from openpyxl import Workbook\nfrom openpyxl.styles import Font, PatternFill, Alignment\n\nwb = Workbook()\nws = wb.active\nws.title = \"Report\"\n\n# Title banner\nws[\"A1\"] = \"Regional Sales Report\"\nws[\"A1\"].font = Font(size=16, bold=True, color=\"1F4E78\")\nws.merge_cells(\"A1:C1\")\n\n# Report-date cell (a fixed label + a value cell we will fill)\nws[\"A2\"] = \"Report date:\"\nws[\"A2\"].font = Font(italic=True)\n\n# Styled header row at row 4\nheaders = [\"Region\", \"Units\", \"Revenue\"]\nheader_fill = PatternFill(\"solid\", fgColor=\"4472C4\")\nfor col, name in enumerate(headers, start=1):\n    cell = ws.cell(row=4, column=col, value=name)\n    cell.font = Font(bold=True, color=\"FFFFFF\")\n    cell.fill = header_fill\n    cell.alignment = Alignment(horizontal=\"center\")\n\n# Totals row with a live formula (data goes in rows 5..9)\nws[\"A10\"] = \"Total\"\nws[\"A10\"].font = Font(bold=True)\nws[\"C10\"] = \"=SUM(C5:C9)\"\nws[\"C10\"].number_format = \"#,##0.00\"\n\nwb.save(\"report_template.xlsx\")\nprint(\"Wrote report_template.xlsx\")\n","python",[28,118,119,135,148,155,167,178,189,194,201,218,267,278,283,289,304,326,331,337,364,390,419,456,483,494,515,520,526,541,562,577,592,597,608],{"__ignoreMap":86},[90,120,121,125,129,132],{"class":92,"line":93},[90,122,124],{"class":123},"szBVR","from",[90,126,128],{"class":127},"sVt8B"," openpyxl ",[90,130,131],{"class":123},"import",[90,133,134],{"class":127}," Workbook\n",[90,136,138,140,143,145],{"class":92,"line":137},2,[90,139,124],{"class":123},[90,141,142],{"class":127}," openpyxl.styles ",[90,144,131],{"class":123},[90,146,147],{"class":127}," Font, PatternFill, Alignment\n",[90,149,151],{"class":92,"line":150},3,[90,152,154],{"emptyLinePlaceholder":153},true,"\n",[90,156,158,161,164],{"class":92,"line":157},4,[90,159,160],{"class":127},"wb ",[90,162,163],{"class":123},"=",[90,165,166],{"class":127}," Workbook()\n",[90,168,170,173,175],{"class":92,"line":169},5,[90,171,172],{"class":127},"ws ",[90,174,163],{"class":123},[90,176,177],{"class":127}," wb.active\n",[90,179,181,184,186],{"class":92,"line":180},6,[90,182,183],{"class":127},"ws.title ",[90,185,163],{"class":123},[90,187,188],{"class":100}," \"Report\"\n",[90,190,192],{"class":92,"line":191},7,[90,193,154],{"emptyLinePlaceholder":153},[90,195,197],{"class":92,"line":196},8,[90,198,200],{"class":199},"sJ8bj","# Title banner\n",[90,202,204,207,210,213,215],{"class":92,"line":203},9,[90,205,206],{"class":127},"ws[",[90,208,209],{"class":100},"\"A1\"",[90,211,212],{"class":127},"] ",[90,214,163],{"class":123},[90,216,217],{"class":100}," \"Regional Sales Report\"\n",[90,219,221,223,225,228,230,233,237,239,243,246,249,251,254,256,259,261,264],{"class":92,"line":220},10,[90,222,206],{"class":127},[90,224,209],{"class":100},[90,226,227],{"class":127},"].font ",[90,229,163],{"class":123},[90,231,232],{"class":127}," Font(",[90,234,236],{"class":235},"s4XuR","size",[90,238,163],{"class":123},[90,240,242],{"class":241},"sj4cs","16",[90,244,245],{"class":127},", ",[90,247,248],{"class":235},"bold",[90,250,163],{"class":123},[90,252,253],{"class":241},"True",[90,255,245],{"class":127},[90,257,258],{"class":235},"color",[90,260,163],{"class":123},[90,262,263],{"class":100},"\"1F4E78\"",[90,265,266],{"class":127},")\n",[90,268,270,273,276],{"class":92,"line":269},11,[90,271,272],{"class":127},"ws.merge_cells(",[90,274,275],{"class":100},"\"A1:C1\"",[90,277,266],{"class":127},[90,279,281],{"class":92,"line":280},12,[90,282,154],{"emptyLinePlaceholder":153},[90,284,286],{"class":92,"line":285},13,[90,287,288],{"class":199},"# Report-date cell (a fixed label + a value cell we will fill)\n",[90,290,292,294,297,299,301],{"class":92,"line":291},14,[90,293,206],{"class":127},[90,295,296],{"class":100},"\"A2\"",[90,298,212],{"class":127},[90,300,163],{"class":123},[90,302,303],{"class":100}," \"Report date:\"\n",[90,305,307,309,311,313,315,317,320,322,324],{"class":92,"line":306},15,[90,308,206],{"class":127},[90,310,296],{"class":100},[90,312,227],{"class":127},[90,314,163],{"class":123},[90,316,232],{"class":127},[90,318,319],{"class":235},"italic",[90,321,163],{"class":123},[90,323,253],{"class":241},[90,325,266],{"class":127},[90,327,329],{"class":92,"line":328},16,[90,330,154],{"emptyLinePlaceholder":153},[90,332,334],{"class":92,"line":333},17,[90,335,336],{"class":199},"# Styled header row at row 4\n",[90,338,340,343,345,348,351,353,356,358,361],{"class":92,"line":339},18,[90,341,342],{"class":127},"headers ",[90,344,163],{"class":123},[90,346,347],{"class":127}," [",[90,349,350],{"class":100},"\"Region\"",[90,352,245],{"class":127},[90,354,355],{"class":100},"\"Units\"",[90,357,245],{"class":127},[90,359,360],{"class":100},"\"Revenue\"",[90,362,363],{"class":127},"]\n",[90,365,367,370,372,375,378,380,383,385,388],{"class":92,"line":366},19,[90,368,369],{"class":127},"header_fill ",[90,371,163],{"class":123},[90,373,374],{"class":127}," PatternFill(",[90,376,377],{"class":100},"\"solid\"",[90,379,245],{"class":127},[90,381,382],{"class":235},"fgColor",[90,384,163],{"class":123},[90,386,387],{"class":100},"\"4472C4\"",[90,389,266],{"class":127},[90,391,393,396,399,402,405,408,411,413,416],{"class":92,"line":392},20,[90,394,395],{"class":123},"for",[90,397,398],{"class":127}," col, name ",[90,400,401],{"class":123},"in",[90,403,404],{"class":241}," enumerate",[90,406,407],{"class":127},"(headers, ",[90,409,410],{"class":235},"start",[90,412,163],{"class":123},[90,414,415],{"class":241},"1",[90,417,418],{"class":127},"):\n",[90,420,422,425,427,430,433,435,438,440,443,445,448,451,453],{"class":92,"line":421},21,[90,423,424],{"class":127},"    cell ",[90,426,163],{"class":123},[90,428,429],{"class":127}," ws.cell(",[90,431,432],{"class":235},"row",[90,434,163],{"class":123},[90,436,437],{"class":241},"4",[90,439,245],{"class":127},[90,441,442],{"class":235},"column",[90,444,163],{"class":123},[90,446,447],{"class":127},"col, ",[90,449,450],{"class":235},"value",[90,452,163],{"class":123},[90,454,455],{"class":127},"name)\n",[90,457,459,462,464,466,468,470,472,474,476,478,481],{"class":92,"line":458},22,[90,460,461],{"class":127},"    cell.font ",[90,463,163],{"class":123},[90,465,232],{"class":127},[90,467,248],{"class":235},[90,469,163],{"class":123},[90,471,253],{"class":241},[90,473,245],{"class":127},[90,475,258],{"class":235},[90,477,163],{"class":123},[90,479,480],{"class":100},"\"FFFFFF\"",[90,482,266],{"class":127},[90,484,486,489,491],{"class":92,"line":485},23,[90,487,488],{"class":127},"    cell.fill ",[90,490,163],{"class":123},[90,492,493],{"class":127}," header_fill\n",[90,495,497,500,502,505,508,510,513],{"class":92,"line":496},24,[90,498,499],{"class":127},"    cell.alignment ",[90,501,163],{"class":123},[90,503,504],{"class":127}," Alignment(",[90,506,507],{"class":235},"horizontal",[90,509,163],{"class":123},[90,511,512],{"class":100},"\"center\"",[90,514,266],{"class":127},[90,516,518],{"class":92,"line":517},25,[90,519,154],{"emptyLinePlaceholder":153},[90,521,523],{"class":92,"line":522},26,[90,524,525],{"class":199},"# Totals row with a live formula (data goes in rows 5..9)\n",[90,527,529,531,534,536,538],{"class":92,"line":528},27,[90,530,206],{"class":127},[90,532,533],{"class":100},"\"A10\"",[90,535,212],{"class":127},[90,537,163],{"class":123},[90,539,540],{"class":100}," \"Total\"\n",[90,542,544,546,548,550,552,554,556,558,560],{"class":92,"line":543},28,[90,545,206],{"class":127},[90,547,533],{"class":100},[90,549,227],{"class":127},[90,551,163],{"class":123},[90,553,232],{"class":127},[90,555,248],{"class":235},[90,557,163],{"class":123},[90,559,253],{"class":241},[90,561,266],{"class":127},[90,563,565,567,570,572,574],{"class":92,"line":564},29,[90,566,206],{"class":127},[90,568,569],{"class":100},"\"C10\"",[90,571,212],{"class":127},[90,573,163],{"class":123},[90,575,576],{"class":100}," \"=SUM(C5:C9)\"\n",[90,578,580,582,584,587,589],{"class":92,"line":579},30,[90,581,206],{"class":127},[90,583,569],{"class":100},[90,585,586],{"class":127},"].number_format ",[90,588,163],{"class":123},[90,590,591],{"class":100}," \"#,##0.00\"\n",[90,593,595],{"class":92,"line":594},31,[90,596,154],{"emptyLinePlaceholder":153},[90,598,600,603,606],{"class":92,"line":599},32,[90,601,602],{"class":127},"wb.save(",[90,604,605],{"class":100},"\"report_template.xlsx\"",[90,607,266],{"class":127},[90,609,611,614,617,620],{"class":92,"line":610},33,[90,612,613],{"class":241},"print",[90,615,616],{"class":127},"(",[90,618,619],{"class":100},"\"Wrote report_template.xlsx\"",[90,621,266],{"class":127},[20,623,625],{"id":624},"the-core-pattern-load-write-data-cells-save-a-dated-copy","The core pattern: load, write data cells, save a dated copy",[10,627,628],{},"Now treat that file as read-only input. Load it, write into the known data cells, and save under a date-stamped name so each run is archived and the template stays pristine:",[81,630,632],{"className":114,"code":631,"language":116,"meta":86,"style":86},"from datetime import date\nfrom openpyxl import load_workbook\n\ntemplate = \"report_template.xlsx\"\nwb = load_workbook(template)\nws = wb[\"Report\"]\n\n# Fill the report-date value cell next to its label\nws[\"B2\"] = date.today().isoformat()\n\n# Save as a NEW file — never back over the template\nout = f\"sales_report_{date.today():%Y%m%d}.xlsx\"\nwb.save(out)\nprint(f\"Wrote {out}\")\n",[28,633,634,646,657,661,671,680,694,698,703,717,721,726,767,772],{"__ignoreMap":86},[90,635,636,638,641,643],{"class":92,"line":93},[90,637,124],{"class":123},[90,639,640],{"class":127}," datetime ",[90,642,131],{"class":123},[90,644,645],{"class":127}," date\n",[90,647,648,650,652,654],{"class":92,"line":137},[90,649,124],{"class":123},[90,651,128],{"class":127},[90,653,131],{"class":123},[90,655,656],{"class":127}," load_workbook\n",[90,658,659],{"class":92,"line":150},[90,660,154],{"emptyLinePlaceholder":153},[90,662,663,666,668],{"class":92,"line":157},[90,664,665],{"class":127},"template ",[90,667,163],{"class":123},[90,669,670],{"class":100}," \"report_template.xlsx\"\n",[90,672,673,675,677],{"class":92,"line":169},[90,674,160],{"class":127},[90,676,163],{"class":123},[90,678,679],{"class":127}," load_workbook(template)\n",[90,681,682,684,686,689,692],{"class":92,"line":180},[90,683,172],{"class":127},[90,685,163],{"class":123},[90,687,688],{"class":127}," wb[",[90,690,691],{"class":100},"\"Report\"",[90,693,363],{"class":127},[90,695,696],{"class":92,"line":191},[90,697,154],{"emptyLinePlaceholder":153},[90,699,700],{"class":92,"line":196},[90,701,702],{"class":199},"# Fill the report-date value cell next to its label\n",[90,704,705,707,710,712,714],{"class":92,"line":203},[90,706,206],{"class":127},[90,708,709],{"class":100},"\"B2\"",[90,711,212],{"class":127},[90,713,163],{"class":123},[90,715,716],{"class":127}," date.today().isoformat()\n",[90,718,719],{"class":92,"line":220},[90,720,154],{"emptyLinePlaceholder":153},[90,722,723],{"class":92,"line":269},[90,724,725],{"class":199},"# Save as a NEW file — never back over the template\n",[90,727,728,731,733,736,739,742,745,748,751,753,756,758,761,764],{"class":92,"line":280},[90,729,730],{"class":127},"out ",[90,732,163],{"class":123},[90,734,735],{"class":123}," f",[90,737,738],{"class":100},"\"sales_report_",[90,740,741],{"class":241},"{",[90,743,744],{"class":127},"date.today():",[90,746,747],{"class":123},"%",[90,749,750],{"class":127},"Y",[90,752,747],{"class":123},[90,754,755],{"class":127},"m",[90,757,747],{"class":123},[90,759,760],{"class":127},"d",[90,762,763],{"class":241},"}",[90,765,766],{"class":100},".xlsx\"\n",[90,768,769],{"class":92,"line":285},[90,770,771],{"class":127},"wb.save(out)\n",[90,773,774,776,778,781,784,786,789,791,794],{"class":92,"line":291},[90,775,613],{"class":241},[90,777,616],{"class":127},[90,779,780],{"class":123},"f",[90,782,783],{"class":100},"\"Wrote ",[90,785,741],{"class":241},[90,787,788],{"class":127},"out",[90,790,763],{"class":241},[90,792,793],{"class":100},"\"",[90,795,266],{"class":127},[10,797,798,799,801],{},"The dated filename matters: it gives you an audit trail, prevents one run from clobbering the previous one, and makes the template reusable forever. Treat ",[28,800,110],{}," as you would a source file under version control — read it, never write it.",[20,803,805],{"id":804},"fill-a-table-region-from-a-dataframe-row-by-row","Fill a table region from a DataFrame, row by row",[10,807,808],{},"The repeating body of most reports is a table. Map each DataFrame row to a worksheet row, starting at the first data row beneath your styled header. Because you write into existing cells, the header styling, the totals formula, and the print area all survive:",[81,810,812],{"className":114,"code":811,"language":116,"meta":86,"style":86},"from datetime import date\nimport pandas as pd\nfrom openpyxl import load_workbook\n\n# Stand-in for your real query result\ndata = pd.DataFrame({\n    \"region\": [\"North\", \"South\", \"West\"],\n    \"units\": [120, 95, 143],\n    \"revenue\": [15990.00, 12047.50, 18744.25],\n})\n\nwb = load_workbook(\"report_template.xlsx\")\nws = wb[\"Report\"]\nws[\"B2\"] = date.today().isoformat()\n\nSTART_ROW = 5   # first row under the header at row 4\nfor offset, record in enumerate(data.itertuples(index=False)):\n    r = START_ROW + offset\n    ws.cell(row=r, column=1, value=record.region)\n    ws.cell(row=r, column=2, value=record.units)\n    ws.cell(row=r, column=3, value=record.revenue)\n\nout = f\"sales_report_{date.today():%Y%m%d}.xlsx\"\nwb.save(out)\nprint(f\"Wrote {len(data)} rows to {out}\")\n",[28,813,814,824,837,847,851,856,866,890,912,934,939,943,956,968,980,984,998,1023,1039,1066,1092,1118,1122,1152,1156],{"__ignoreMap":86},[90,815,816,818,820,822],{"class":92,"line":93},[90,817,124],{"class":123},[90,819,640],{"class":127},[90,821,131],{"class":123},[90,823,645],{"class":127},[90,825,826,828,831,834],{"class":92,"line":137},[90,827,131],{"class":123},[90,829,830],{"class":127}," pandas ",[90,832,833],{"class":123},"as",[90,835,836],{"class":127}," pd\n",[90,838,839,841,843,845],{"class":92,"line":150},[90,840,124],{"class":123},[90,842,128],{"class":127},[90,844,131],{"class":123},[90,846,656],{"class":127},[90,848,849],{"class":92,"line":157},[90,850,154],{"emptyLinePlaceholder":153},[90,852,853],{"class":92,"line":169},[90,854,855],{"class":199},"# Stand-in for your real query result\n",[90,857,858,861,863],{"class":92,"line":180},[90,859,860],{"class":127},"data ",[90,862,163],{"class":123},[90,864,865],{"class":127}," pd.DataFrame({\n",[90,867,868,871,874,877,879,882,884,887],{"class":92,"line":191},[90,869,870],{"class":100},"    \"region\"",[90,872,873],{"class":127},": [",[90,875,876],{"class":100},"\"North\"",[90,878,245],{"class":127},[90,880,881],{"class":100},"\"South\"",[90,883,245],{"class":127},[90,885,886],{"class":100},"\"West\"",[90,888,889],{"class":127},"],\n",[90,891,892,895,897,900,902,905,907,910],{"class":92,"line":196},[90,893,894],{"class":100},"    \"units\"",[90,896,873],{"class":127},[90,898,899],{"class":241},"120",[90,901,245],{"class":127},[90,903,904],{"class":241},"95",[90,906,245],{"class":127},[90,908,909],{"class":241},"143",[90,911,889],{"class":127},[90,913,914,917,919,922,924,927,929,932],{"class":92,"line":203},[90,915,916],{"class":100},"    \"revenue\"",[90,918,873],{"class":127},[90,920,921],{"class":241},"15990.00",[90,923,245],{"class":127},[90,925,926],{"class":241},"12047.50",[90,928,245],{"class":127},[90,930,931],{"class":241},"18744.25",[90,933,889],{"class":127},[90,935,936],{"class":92,"line":220},[90,937,938],{"class":127},"})\n",[90,940,941],{"class":92,"line":269},[90,942,154],{"emptyLinePlaceholder":153},[90,944,945,947,949,952,954],{"class":92,"line":280},[90,946,160],{"class":127},[90,948,163],{"class":123},[90,950,951],{"class":127}," load_workbook(",[90,953,605],{"class":100},[90,955,266],{"class":127},[90,957,958,960,962,964,966],{"class":92,"line":285},[90,959,172],{"class":127},[90,961,163],{"class":123},[90,963,688],{"class":127},[90,965,691],{"class":100},[90,967,363],{"class":127},[90,969,970,972,974,976,978],{"class":92,"line":291},[90,971,206],{"class":127},[90,973,709],{"class":100},[90,975,212],{"class":127},[90,977,163],{"class":123},[90,979,716],{"class":127},[90,981,982],{"class":92,"line":306},[90,983,154],{"emptyLinePlaceholder":153},[90,985,986,989,992,995],{"class":92,"line":328},[90,987,988],{"class":241},"START_ROW",[90,990,991],{"class":123}," =",[90,993,994],{"class":241}," 5",[90,996,997],{"class":199},"   # first row under the header at row 4\n",[90,999,1000,1002,1005,1007,1009,1012,1015,1017,1020],{"class":92,"line":333},[90,1001,395],{"class":123},[90,1003,1004],{"class":127}," offset, record ",[90,1006,401],{"class":123},[90,1008,404],{"class":241},[90,1010,1011],{"class":127},"(data.itertuples(",[90,1013,1014],{"class":235},"index",[90,1016,163],{"class":123},[90,1018,1019],{"class":241},"False",[90,1021,1022],{"class":127},")):\n",[90,1024,1025,1028,1030,1033,1036],{"class":92,"line":339},[90,1026,1027],{"class":127},"    r ",[90,1029,163],{"class":123},[90,1031,1032],{"class":241}," START_ROW",[90,1034,1035],{"class":123}," +",[90,1037,1038],{"class":127}," offset\n",[90,1040,1041,1044,1046,1048,1051,1053,1055,1057,1059,1061,1063],{"class":92,"line":366},[90,1042,1043],{"class":127},"    ws.cell(",[90,1045,432],{"class":235},[90,1047,163],{"class":123},[90,1049,1050],{"class":127},"r, ",[90,1052,442],{"class":235},[90,1054,163],{"class":123},[90,1056,415],{"class":241},[90,1058,245],{"class":127},[90,1060,450],{"class":235},[90,1062,163],{"class":123},[90,1064,1065],{"class":127},"record.region)\n",[90,1067,1068,1070,1072,1074,1076,1078,1080,1083,1085,1087,1089],{"class":92,"line":392},[90,1069,1043],{"class":127},[90,1071,432],{"class":235},[90,1073,163],{"class":123},[90,1075,1050],{"class":127},[90,1077,442],{"class":235},[90,1079,163],{"class":123},[90,1081,1082],{"class":241},"2",[90,1084,245],{"class":127},[90,1086,450],{"class":235},[90,1088,163],{"class":123},[90,1090,1091],{"class":127},"record.units)\n",[90,1093,1094,1096,1098,1100,1102,1104,1106,1109,1111,1113,1115],{"class":92,"line":421},[90,1095,1043],{"class":127},[90,1097,432],{"class":235},[90,1099,163],{"class":123},[90,1101,1050],{"class":127},[90,1103,442],{"class":235},[90,1105,163],{"class":123},[90,1107,1108],{"class":241},"3",[90,1110,245],{"class":127},[90,1112,450],{"class":235},[90,1114,163],{"class":123},[90,1116,1117],{"class":127},"record.revenue)\n",[90,1119,1120],{"class":92,"line":458},[90,1121,154],{"emptyLinePlaceholder":153},[90,1123,1124,1126,1128,1130,1132,1134,1136,1138,1140,1142,1144,1146,1148,1150],{"class":92,"line":485},[90,1125,730],{"class":127},[90,1127,163],{"class":123},[90,1129,735],{"class":123},[90,1131,738],{"class":100},[90,1133,741],{"class":241},[90,1135,744],{"class":127},[90,1137,747],{"class":123},[90,1139,750],{"class":127},[90,1141,747],{"class":123},[90,1143,755],{"class":127},[90,1145,747],{"class":123},[90,1147,760],{"class":127},[90,1149,763],{"class":241},[90,1151,766],{"class":100},[90,1153,1154],{"class":92,"line":496},[90,1155,771],{"class":127},[90,1157,1158,1160,1162,1164,1166,1169,1172,1174,1177,1179,1181,1183,1185],{"class":92,"line":517},[90,1159,613],{"class":241},[90,1161,616],{"class":127},[90,1163,780],{"class":123},[90,1165,783],{"class":100},[90,1167,1168],{"class":241},"{len",[90,1170,1171],{"class":127},"(data)",[90,1173,763],{"class":241},[90,1175,1176],{"class":100}," rows to ",[90,1178,741],{"class":241},[90,1180,788],{"class":127},[90,1182,763],{"class":241},[90,1184,793],{"class":100},[90,1186,266],{"class":127},[10,1188,1189,1190,57],{},"The deeper mechanics of this loop — choosing the start row, handling DataFrames vs. plain lists, and avoiding off-by-one mistakes — are covered in ",[14,1191,1193],{"href":1192},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Ffill-excel-template-with-python-openpyxl\u002F","Fill an Excel Template with Python and openpyxl",[20,1195,1197],{"id":1196},"keep-formulas-intact-write-values-not-over-formulas","Keep formulas intact — write values, not over formulas",[10,1199,1200,1201,1204,1205,1208,1209,1212,1213,1215],{},"The totals cell ",[28,1202,1203],{},"C10"," holds ",[28,1206,1207],{},"=SUM(C5:C9)",". You never write to it; you write the data it sums and let Excel recalculate when the file opens. The rule is simple: ",[46,1210,1211],{},"write only to cells that hold data, never to cells that hold formulas."," If you assign a number to ",[28,1214,1203],{},", you overwrite the formula with a static value and the report stops being live.",[10,1217,1218,1220,1221,1224,1225,1228],{},[28,1219,52],{}," does not evaluate formulas — it stores the formula string and Excel computes the result on open. If you need the computed value inside Python (rare for templates), reload with ",[28,1222,1223],{},"load_workbook(path, data_only=True)",", but note that returns the last value Excel cached, which is ",[28,1226,1227],{},"None"," for a file that has never been opened in Excel.",[20,1230,1232,1233,1236],{"id":1231},"why-pandasto_excel-is-the-wrong-tool-for-templates","Why ",[28,1234,1235],{},"pandas.to_excel"," is the wrong tool for templates",[10,1238,1239,1240,1243,1244,1246,1247,1249],{},"It is tempting to reach for ",[28,1241,1242],{},"df.to_excel(\"report_template.xlsx\", ...)",". Do not. ",[28,1245,1235],{}," with the default ",[28,1248,52],{}," engine creates a brand-new worksheet and writes a plain grid into it. It does not edit your template in place — it replaces the sheet contents wholesale, discarding the title banner, the header fill, the merged cells, the totals formula, and the print area. The result opens, but every bit of design work is gone.",[1251,1252,1253,1270],"table",{},[1254,1255,1256],"thead",{},[1257,1258,1259,1263,1267],"tr",{},[1260,1261,1262],"th",{},"Approach",[1260,1264,1266],{"align":1265},"center","Preserves template styling?",[1260,1268,1269],{},"What it actually does",[1271,1272,1273,1291,1304],"tbody",{},[1257,1274,1275,1285,1288],{},[1276,1277,1278,1280,1281,1284],"td",{},[28,1279,52],{}," ",[28,1282,1283],{},"load_workbook"," + write cells",[1276,1286,1287],{"align":1265},"yes",[1276,1289,1290],{},"edits existing cells, leaves the rest untouched",[1257,1292,1293,1298,1301],{},[1276,1294,1295],{},[28,1296,1297],{},"pandas.to_excel(path)",[1276,1299,1300],{"align":1265},"no",[1276,1302,1303],{},"writes a fresh, unstyled sheet over your file",[1257,1305,1306,1312,1314],{},[1276,1307,1308,1311],{},[28,1309,1310],{},"xlsxwriter"," engine",[1276,1313,1300],{"align":1265},[1276,1315,1316],{},"cannot open or edit an existing workbook at all",[10,1318,1319,1320,57],{},"Preserving formatting is the whole point of templates, and it is subtle enough to deserve its own page: see ",[14,1321,1323],{"href":1322},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","Populate an Excel Template Without Losing Formatting",[20,1325,1327,1328,1331,1332,1334],{"id":1326},"macro-templates-and-the-xltx-vs-xlsx-question","Macro templates and the ",[28,1329,1330],{},".xltx"," vs ",[28,1333,37],{}," question",[10,1336,1337,1338,1340,1341,1344,1345,1347,1348,1352,1353,1355,1356,1358,1359,1361,1362,1364,1365,1367],{},"Excel has a dedicated template format, ",[28,1339,1330],{}," (and ",[28,1342,1343],{},".xltm"," for macro-enabled templates). When a user double-clicks an ",[28,1346,1330],{},", Excel opens a ",[1349,1350,1351],"em",{},"copy"," rather than the original — exactly the no-overwrite behavior you want. ",[28,1354,52],{}," can ",[28,1357,56],{}," an ",[28,1360,1330],{},"; just save your output as ",[28,1363,37],{},". A plain ",[28,1366,37],{}," works equally well as a template as long as your script enforces the save-as-new-file discipline itself.",[10,1369,1370,1371,1374,1375,1377,1378,1381],{},"If the template carries VBA macros (an ",[28,1372,1373],{},".xlsm"," or ",[28,1376,1343],{}," file), pass ",[28,1379,1380],{},"keep_vba=True"," so the macro project survives the round-trip:",[81,1383,1385],{"className":114,"code":1384,"language":116,"meta":86,"style":86},"from openpyxl import load_workbook\n\n# For a macro-enabled template, preserve the VBA project\nwb = load_workbook(\"macro_template.xlsm\", keep_vba=True)\nws = wb.active\nws[\"B2\"] = \"2026-06-18\"\nwb.save(\"report_with_macros.xlsm\")   # macros intact\nprint(\"Saved macro-enabled report\")\n",[28,1386,1387,1397,1401,1406,1428,1436,1449,1462],{"__ignoreMap":86},[90,1388,1389,1391,1393,1395],{"class":92,"line":93},[90,1390,124],{"class":123},[90,1392,128],{"class":127},[90,1394,131],{"class":123},[90,1396,656],{"class":127},[90,1398,1399],{"class":92,"line":137},[90,1400,154],{"emptyLinePlaceholder":153},[90,1402,1403],{"class":92,"line":150},[90,1404,1405],{"class":199},"# For a macro-enabled template, preserve the VBA project\n",[90,1407,1408,1410,1412,1414,1417,1419,1422,1424,1426],{"class":92,"line":157},[90,1409,160],{"class":127},[90,1411,163],{"class":123},[90,1413,951],{"class":127},[90,1415,1416],{"class":100},"\"macro_template.xlsm\"",[90,1418,245],{"class":127},[90,1420,1421],{"class":235},"keep_vba",[90,1423,163],{"class":123},[90,1425,253],{"class":241},[90,1427,266],{"class":127},[90,1429,1430,1432,1434],{"class":92,"line":169},[90,1431,172],{"class":127},[90,1433,163],{"class":123},[90,1435,177],{"class":127},[90,1437,1438,1440,1442,1444,1446],{"class":92,"line":180},[90,1439,206],{"class":127},[90,1441,709],{"class":100},[90,1443,212],{"class":127},[90,1445,163],{"class":123},[90,1447,1448],{"class":100}," \"2026-06-18\"\n",[90,1450,1451,1453,1456,1459],{"class":92,"line":191},[90,1452,602],{"class":127},[90,1454,1455],{"class":100},"\"report_with_macros.xlsm\"",[90,1457,1458],{"class":127},")   ",[90,1460,1461],{"class":199},"# macros intact\n",[90,1463,1464,1466,1468,1471],{"class":92,"line":196},[90,1465,613],{"class":241},[90,1467,616],{"class":127},[90,1469,1470],{"class":100},"\"Saved macro-enabled report\"",[90,1472,266],{"class":127},[10,1474,1475,1476,245,1478,1480],{},"Without ",[28,1477,1380],{},[28,1479,52],{}," strips the macros on save and Excel warns the file is corrupt.",[20,1482,1484],{"id":1483},"frequently-asked-questions","Frequently asked questions",[10,1486,1487,1490,1491,1494,1495,1498],{},[46,1488,1489],{},"Can I add more rows than the template's formula range covers?","\nYes, but extend the formula too. If data fills rows 5–12 and the totals formula only sums ",[28,1492,1493],{},"C5:C9",", update it: ",[28,1496,1497],{},"ws[\"C13\"] = \"=SUM(C5:C12)\"",". Better, size the template's data region for your largest expected run, or compute the range in code from your row count.",[10,1500,1501,1504,1505,1507,1508,1511],{},[46,1502,1503],{},"Does openpyxl preserve charts and images in the template?","\nModern ",[28,1506,52],{}," preserves most charts and images on a load-edit-save round-trip, but it has historically dropped some objects (pivot caches, certain chart types). Write only data cells, keep the template simple, and open one output file to verify before trusting the pipeline. The ",[14,1509,1510],{"href":1322},"formatting preservation page"," covers exactly what survives.",[10,1513,1514,1517],{},[46,1515,1516],{},"Should the template live in my repo?","\nYes. Commit it alongside the script and treat it as code. That way a styling change is a reviewable diff, and every run uses a known version.",[20,1519,1521],{"id":1520},"conclusion","Conclusion",[10,1523,1524,1525,1527,1528,1530],{},"Template injection keeps responsibilities where they belong: the ",[28,1526,37],{}," owns appearance, your script owns data. Load the prepared file, write only the data cells, leave formulas and styling alone, and save a dated copy so the template stays pristine. Avoid ",[28,1529,1235],{}," for this job — it rewrites the sheet and discards everything that made the template worth keeping. The two long-tail guides below drill into the two skills this pattern depends on: filling a table region cleanly, and doing it without losing a single style.",[20,1532,1534],{"id":1533},"where-to-go-next","Where to go next",[10,1536,1537],{},"Start with the pillar overview, then work through this cluster's long-tails and a related sibling:",[1539,1540,1541,1546,1551,1556,1563,1570],"ul",{},[43,1542,1543,1545],{},[14,1544,17],{"href":16}," — the full ingest → transform → generate → deliver pipeline this cluster's generate stage fits into.",[43,1547,1548,1550],{},[14,1549,1193],{"href":1192}," — the step-by-step mechanics of writing named cells and a repeating data table.",[43,1552,1553,1555],{},[14,1554,1323],{"href":1322}," — what openpyxl preserves and why pandas does not.",[43,1557,1558,1562],{},[14,1559,1561],{"href":1560},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002F","Building Multi-Sheet Excel Dashboards"," — when one report becomes several linked sheets.",[43,1564,1565,1569],{},[14,1566,1568],{"href":1567},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002F","Exporting Excel Reports to PDF"," — turning the finished template into a shareable PDF.",[43,1571,1572,1576],{},[14,1573,1575],{"href":1574},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","Styling Excel Cells with openpyxl"," — for when you do need to build styling in code instead of in a template.",[1578,1579,1580],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":86,"searchDepth":137,"depth":137,"links":1582},[1583,1584,1585,1586,1587,1588,1590,1592,1593,1594],{"id":22,"depth":137,"text":23},{"id":75,"depth":137,"text":76},{"id":624,"depth":137,"text":625},{"id":804,"depth":137,"text":805},{"id":1196,"depth":137,"text":1197},{"id":1231,"depth":137,"text":1589},"Why pandas.to_excel is the wrong tool for templates",{"id":1326,"depth":137,"text":1591},"Macro templates and the .xltx vs .xlsx question",{"id":1483,"depth":137,"text":1484},{"id":1520,"depth":137,"text":1521},{"id":1533,"depth":137,"text":1534},"Inject data into a prepared .xlsx with openpyxl instead of rebuilding sheets from scratch — preserve branding, formulas, and print areas, then save a dated copy.","md",{},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates",{"title":1600,"description":1601},"Generate Excel Reports from Templates","Fill a designer's .xlsx template with Python and openpyxl: write only data cells, keep formulas and styling intact, save dated copies, and handle macro templates.","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Findex","SbphCo6Du-FuS4yZgq0oE_eVIPtmD0Nzs1FJY4M4Bn8",[1605,1609],{"title":1606,"path":1607,"stem":1608,"children":-1},"Convert an Excel File to PDF with Python","\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002Findex",{"title":1193,"path":1610,"stem":1611,"children":-1},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Ffill-excel-template-with-python-openpyxl","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Ffill-excel-template-with-python-openpyxl\u002Findex",1781773160643]