[{"data":1,"prerenderedAt":1905},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template":3,"surround:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template":1896},{"id":4,"title":5,"body":6,"dateModified":1873,"datePublished":1873,"description":1874,"extension":1875,"faq":1876,"meta":1887,"navigation":207,"path":1888,"seo":1889,"slug":1892,"stem":1893,"type":1894,"__hash__":1895},"docs\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template\u002Findex.md","Keep Charts and Images When Filling an Excel Template",{"type":7,"value":8,"toc":1857},"minimark",[9,24,136,141,172,176,179,399,406,409,413,424,575,578,582,585,823,830,921,925,928,1072,1079,1083,1086,1150,1157,1161,1168,1172,1175,1420,1427,1431,1445,1614,1620,1708,1711,1715,1757,1761,1768,1772,1775,1779,1785,1791,1797,1806,1812,1816,1853],[10,11,12,13,17,18,23],"p",{},"You fill a template with openpyxl, save it, and the chart your colleague spent an afternoon on has vanished. Nothing errored. The cause is structural: openpyxl parses a workbook into an object model and writes a ",[14,15,16],"em",{},"new"," file from that model, so anything it does not represent has nothing to write back. This guide explains exactly what survives, then gives three practical ways to fill a template without losing its visuals — ranked by how much control you have over the template itself. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002F","Generating Excel Reports from Templates",".",[25,26,34,35,34,39,34,43,34,50,34,57,34,65,34,71,34,76,34,80,34,88,34,93,34,98,34,103,34,107,34,111,34,115,34,118,34,122,34,127,34,129,34,132],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"0 0 760 240","img",[30,31],"tp-t","tp-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[36,37,38],"title",{"id":30},"Why a re-save loses objects",[40,41,42],"desc",{"id":31},"openpyxl parses the file into an object model and writes a new file from it, so any part of the original that the model does not represent is simply absent from the output.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","240","#ffffff",[51,52,56],"text",{"x":53,"y":54,"style":55},"380","26","font-size:13px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","The output is rebuilt, not edited",[44,58],{"x":54,"y":59,"width":60,"height":61,"rx":62,"fill":63,"stroke":64},"66","176","110","12","#f0f2f5","var(--line,#cdd5e6)",[51,66,70],{"x":67,"y":68,"style":69},"114","96","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","template.xlsx",[51,72,75],{"x":67,"y":73,"style":74},"122","font-size:11.5px;fill:var(--muted,#5b6780);text-anchor:middle","cells, styles",[51,77,79],{"x":67,"y":78,"style":74},"144","chart, image, pivot",[81,82],"line",{"x1":83,"y1":84,"x2":85,"y2":84,"stroke":86,"style":87},"202","121","238","var(--brand,#5b5cf0)","stroke-width:2px",[89,90],"polygon",{"points":91,"fill":92},"238,121 228,116 228,126","#5b5cf0",[44,94],{"x":95,"y":59,"width":96,"height":61,"rx":62,"fill":97,"stroke":64},"242","196","#ebebfd",[51,99,102],{"x":100,"y":68,"style":101},"340","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","openpyxl model",[51,104,106],{"x":100,"y":73,"style":105},"font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","cells, styles ✓",[51,108,110],{"x":100,"y":78,"style":109},"font-size:11.5px;fill:var(--accent-ink,#be185d);text-anchor:middle","chart, pivot — not held",[81,112],{"x1":113,"y1":84,"x2":114,"y2":84,"stroke":86,"style":87},"438","474",[89,116],{"points":117,"fill":92},"474,121 464,116 464,126",[44,119],{"x":120,"y":59,"width":60,"height":61,"rx":62,"fill":121,"stroke":64},"478","#fee8f2",[51,123,126],{"x":124,"y":68,"style":125},"566","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","output.xlsx",[51,128,75],{"x":124,"y":73,"style":105},[51,130,131],{"x":124,"y":78,"style":109},"visuals gone",[51,133,135],{"x":53,"y":134,"style":74},"212","Nothing errors — the objects simply never reach the writer",[137,138,140],"h2",{"id":139},"prerequisites","Prerequisites",[142,143,148],"pre",{"className":144,"code":145,"language":146,"meta":147,"style":147},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas xlsxwriter\n","bash","",[149,150,151],"code",{"__ignoreMap":147},[152,153,155,159,163,166,169],"span",{"class":81,"line":154},1,[152,156,158],{"class":157},"sMTad","pip",[152,160,162],{"class":161},"srMev"," install",[152,164,165],{"class":161}," openpyxl",[152,167,168],{"class":161}," pandas",[152,170,171],{"class":161}," xlsxwriter\n",[137,173,175],{"id":174},"establish-what-your-template-loses","Establish what your template loses",[10,177,178],{},"Before choosing a strategy, measure. Fill nothing, save, and compare the parts inside the two zip files:",[142,180,184],{"className":181,"code":182,"language":183,"meta":147,"style":147},"language-python shiki shiki-themes github-light github-dark-high-contrast","\"\"\"Diff the internal parts of a template and its re-saved copy.\"\"\"\nimport zipfile\n\nfrom openpyxl import load_workbook\n\nwb = load_workbook(\"template.xlsx\")\nwb.save(\"resaved.xlsx\")\n\ndef parts(path: str) -> set[str]:\n    with zipfile.ZipFile(path) as zf:\n        return {n for n in zf.namelist() if not n.startswith(\"docProps\u002F\")}\n\nlost = parts(\"template.xlsx\") - parts(\"resaved.xlsx\")\nfor name in sorted(lost):\n    print(\"lost:\", name)\n","python",[149,185,186,191,202,209,223,228,246,257,262,287,302,338,343,368,384],{"__ignoreMap":147},[152,187,188],{"class":81,"line":154},[152,189,190],{"class":161},"\"\"\"Diff the internal parts of a template and its re-saved copy.\"\"\"\n",[152,192,194,198],{"class":81,"line":193},2,[152,195,197],{"class":196},"s-kum","import",[152,199,201],{"class":200},"skGVy"," zipfile\n",[152,203,205],{"class":81,"line":204},3,[152,206,208],{"emptyLinePlaceholder":207},true,"\n",[152,210,212,215,218,220],{"class":81,"line":211},4,[152,213,214],{"class":196},"from",[152,216,217],{"class":200}," openpyxl ",[152,219,197],{"class":196},[152,221,222],{"class":200}," load_workbook\n",[152,224,226],{"class":81,"line":225},5,[152,227,208],{"emptyLinePlaceholder":207},[152,229,231,234,237,240,243],{"class":81,"line":230},6,[152,232,233],{"class":200},"wb ",[152,235,236],{"class":196},"=",[152,238,239],{"class":200}," load_workbook(",[152,241,242],{"class":161},"\"template.xlsx\"",[152,244,245],{"class":200},")\n",[152,247,249,252,255],{"class":81,"line":248},7,[152,250,251],{"class":200},"wb.save(",[152,253,254],{"class":161},"\"resaved.xlsx\"",[152,256,245],{"class":200},[152,258,260],{"class":81,"line":259},8,[152,261,208],{"emptyLinePlaceholder":207},[152,263,265,268,272,275,279,282,284],{"class":81,"line":264},9,[152,266,267],{"class":196},"def",[152,269,271],{"class":270},"s_Opv"," parts",[152,273,274],{"class":200},"(path: ",[152,276,278],{"class":277},"sP0c6","str",[152,280,281],{"class":200},") -> set[",[152,283,278],{"class":277},[152,285,286],{"class":200},"]:\n",[152,288,290,293,296,299],{"class":81,"line":289},10,[152,291,292],{"class":196},"    with",[152,294,295],{"class":200}," zipfile.ZipFile(path) ",[152,297,298],{"class":196},"as",[152,300,301],{"class":200}," zf:\n",[152,303,305,308,311,314,317,320,323,326,329,332,335],{"class":81,"line":304},11,[152,306,307],{"class":196},"        return",[152,309,310],{"class":200}," {n ",[152,312,313],{"class":196},"for",[152,315,316],{"class":200}," n ",[152,318,319],{"class":196},"in",[152,321,322],{"class":200}," zf.namelist() ",[152,324,325],{"class":196},"if",[152,327,328],{"class":196}," not",[152,330,331],{"class":200}," n.startswith(",[152,333,334],{"class":161},"\"docProps\u002F\"",[152,336,337],{"class":200},")}\n",[152,339,341],{"class":81,"line":340},12,[152,342,208],{"emptyLinePlaceholder":207},[152,344,346,349,351,354,356,359,362,364,366],{"class":81,"line":345},13,[152,347,348],{"class":200},"lost ",[152,350,236],{"class":196},[152,352,353],{"class":200}," parts(",[152,355,242],{"class":161},[152,357,358],{"class":200},") ",[152,360,361],{"class":196},"-",[152,363,353],{"class":200},[152,365,254],{"class":161},[152,367,245],{"class":200},[152,369,371,373,376,378,381],{"class":81,"line":370},14,[152,372,313],{"class":196},[152,374,375],{"class":200}," name ",[152,377,319],{"class":196},[152,379,380],{"class":277}," sorted",[152,382,383],{"class":200},"(lost):\n",[152,385,387,390,393,396],{"class":81,"line":386},15,[152,388,389],{"class":277},"    print",[152,391,392],{"class":200},"(",[152,394,395],{"class":161},"\"lost:\"",[152,397,398],{"class":200},", name)\n",[142,400,404],{"className":401,"code":403,"language":51,"meta":147},[402],"language-text","lost: xl\u002Fcharts\u002Fchart1.xml\nlost: xl\u002Fdrawings\u002Fdrawing1.xml\nlost: xl\u002FpivotCache\u002FpivotCacheDefinition1.xml\n",[149,405,403],{"__ignoreMap":147},[10,407,408],{},"That list tells you exactly which strategy you need. A template that loses nothing can be filled directly, and many simple templates do survive intact.",[137,410,412],{"id":411},"strategy-1-fill-only-the-cells-let-excel-redraw","Strategy 1: fill only the cells, let Excel redraw",[10,414,415,416,419,420,423],{},"The best outcome is a template whose chart points at a range that grows, so writing data is all your code has to do. Define the source as a dynamic named range in the template — using ",[149,417,418],{},"OFFSET","\u002F",[149,421,422],{},"COUNTA"," or an Excel table — and the chart follows the data without any code touching the chart.",[142,425,427],{"className":181,"code":426,"language":183,"meta":147,"style":147},"from openpyxl import load_workbook\n\nwb = load_workbook(\"template.xlsx\")\nws = wb[\"Data\"]\n\nfor r, (region, revenue) in enumerate(rows, start=2):\n    ws.cell(row=r, column=1, value=region)\n    ws.cell(row=r, column=2, value=float(revenue))\n\nwb.save(\"filled.xlsx\")\n",[149,428,429,439,443,455,471,475,502,534,562,566],{"__ignoreMap":147},[152,430,431,433,435,437],{"class":81,"line":154},[152,432,214],{"class":196},[152,434,217],{"class":200},[152,436,197],{"class":196},[152,438,222],{"class":200},[152,440,441],{"class":81,"line":193},[152,442,208],{"emptyLinePlaceholder":207},[152,444,445,447,449,451,453],{"class":81,"line":204},[152,446,233],{"class":200},[152,448,236],{"class":196},[152,450,239],{"class":200},[152,452,242],{"class":161},[152,454,245],{"class":200},[152,456,457,460,462,465,468],{"class":81,"line":211},[152,458,459],{"class":200},"ws ",[152,461,236],{"class":196},[152,463,464],{"class":200}," wb[",[152,466,467],{"class":161},"\"Data\"",[152,469,470],{"class":200},"]\n",[152,472,473],{"class":81,"line":225},[152,474,208],{"emptyLinePlaceholder":207},[152,476,477,479,482,484,487,490,494,496,499],{"class":81,"line":230},[152,478,313],{"class":196},[152,480,481],{"class":200}," r, (region, revenue) ",[152,483,319],{"class":196},[152,485,486],{"class":277}," enumerate",[152,488,489],{"class":200},"(rows, ",[152,491,493],{"class":492},"sa561","start",[152,495,236],{"class":196},[152,497,498],{"class":277},"2",[152,500,501],{"class":200},"):\n",[152,503,504,507,510,512,515,518,520,523,526,529,531],{"class":81,"line":248},[152,505,506],{"class":200},"    ws.cell(",[152,508,509],{"class":492},"row",[152,511,236],{"class":196},[152,513,514],{"class":200},"r, ",[152,516,517],{"class":492},"column",[152,519,236],{"class":196},[152,521,522],{"class":277},"1",[152,524,525],{"class":200},", ",[152,527,528],{"class":492},"value",[152,530,236],{"class":196},[152,532,533],{"class":200},"region)\n",[152,535,536,538,540,542,544,546,548,550,552,554,556,559],{"class":81,"line":259},[152,537,506],{"class":200},[152,539,509],{"class":492},[152,541,236],{"class":196},[152,543,514],{"class":200},[152,545,517],{"class":492},[152,547,236],{"class":196},[152,549,498],{"class":277},[152,551,525],{"class":200},[152,553,528],{"class":492},[152,555,236],{"class":196},[152,557,558],{"class":277},"float",[152,560,561],{"class":200},"(revenue))\n",[152,563,564],{"class":81,"line":264},[152,565,208],{"emptyLinePlaceholder":207},[152,567,568,570,573],{"class":81,"line":289},[152,569,251],{"class":200},[152,571,572],{"class":161},"\"filled.xlsx\"",[152,574,245],{"class":200},[10,576,577],{},"This works only if the chart survives the round trip, so run the diff above first. Where it does survive — common for charts openpyxl can model — this is by far the cheapest option, and the template's owner keeps control of the visual.",[137,579,581],{"id":580},"strategy-2-rebuild-the-chart-after-filling","Strategy 2: rebuild the chart after filling",[10,583,584],{},"If the chart does not survive, recreate it in code. You lose the designer's fine-tuning, but the output is reliable and reproducible:",[142,586,588],{"className":181,"code":587,"language":183,"meta":147,"style":147},"from openpyxl import load_workbook\nfrom openpyxl.chart import BarChart, Reference\n\nwb = load_workbook(\"template.xlsx\")\nws = wb[\"Data\"]\n\n# … write the data rows as above …\n\nchart = BarChart()\nchart.type = \"col\"\nchart.title = \"Revenue by region\"\nchart.y_axis.title = \"Revenue (GBP)\"\ndata = Reference(ws, min_col=2, min_row=1, max_row=ws.max_row)\ncats = Reference(ws, min_col=1, min_row=2, max_row=ws.max_row)\nchart.add_data(data, titles_from_data=True)\nchart.set_categories(cats)\nchart.height, chart.width = 8, 15\n\nws.add_chart(chart, \"E2\")\nwb.save(\"filled.xlsx\")\n",[149,589,590,600,612,616,628,640,644,650,654,664,674,684,694,730,761,776,782,798,803,814],{"__ignoreMap":147},[152,591,592,594,596,598],{"class":81,"line":154},[152,593,214],{"class":196},[152,595,217],{"class":200},[152,597,197],{"class":196},[152,599,222],{"class":200},[152,601,602,604,607,609],{"class":81,"line":193},[152,603,214],{"class":196},[152,605,606],{"class":200}," openpyxl.chart ",[152,608,197],{"class":196},[152,610,611],{"class":200}," BarChart, Reference\n",[152,613,614],{"class":81,"line":204},[152,615,208],{"emptyLinePlaceholder":207},[152,617,618,620,622,624,626],{"class":81,"line":211},[152,619,233],{"class":200},[152,621,236],{"class":196},[152,623,239],{"class":200},[152,625,242],{"class":161},[152,627,245],{"class":200},[152,629,630,632,634,636,638],{"class":81,"line":225},[152,631,459],{"class":200},[152,633,236],{"class":196},[152,635,464],{"class":200},[152,637,467],{"class":161},[152,639,470],{"class":200},[152,641,642],{"class":81,"line":230},[152,643,208],{"emptyLinePlaceholder":207},[152,645,646],{"class":81,"line":248},[152,647,649],{"class":648},"s-wDw","# … write the data rows as above …\n",[152,651,652],{"class":81,"line":259},[152,653,208],{"emptyLinePlaceholder":207},[152,655,656,659,661],{"class":81,"line":264},[152,657,658],{"class":200},"chart ",[152,660,236],{"class":196},[152,662,663],{"class":200}," BarChart()\n",[152,665,666,669,671],{"class":81,"line":289},[152,667,668],{"class":200},"chart.type ",[152,670,236],{"class":196},[152,672,673],{"class":161}," \"col\"\n",[152,675,676,679,681],{"class":81,"line":304},[152,677,678],{"class":200},"chart.title ",[152,680,236],{"class":196},[152,682,683],{"class":161}," \"Revenue by region\"\n",[152,685,686,689,691],{"class":81,"line":340},[152,687,688],{"class":200},"chart.y_axis.title ",[152,690,236],{"class":196},[152,692,693],{"class":161}," \"Revenue (GBP)\"\n",[152,695,696,699,701,704,707,709,711,713,716,718,720,722,725,727],{"class":81,"line":345},[152,697,698],{"class":200},"data ",[152,700,236],{"class":196},[152,702,703],{"class":200}," Reference(ws, ",[152,705,706],{"class":492},"min_col",[152,708,236],{"class":196},[152,710,498],{"class":277},[152,712,525],{"class":200},[152,714,715],{"class":492},"min_row",[152,717,236],{"class":196},[152,719,522],{"class":277},[152,721,525],{"class":200},[152,723,724],{"class":492},"max_row",[152,726,236],{"class":196},[152,728,729],{"class":200},"ws.max_row)\n",[152,731,732,735,737,739,741,743,745,747,749,751,753,755,757,759],{"class":81,"line":370},[152,733,734],{"class":200},"cats ",[152,736,236],{"class":196},[152,738,703],{"class":200},[152,740,706],{"class":492},[152,742,236],{"class":196},[152,744,522],{"class":277},[152,746,525],{"class":200},[152,748,715],{"class":492},[152,750,236],{"class":196},[152,752,498],{"class":277},[152,754,525],{"class":200},[152,756,724],{"class":492},[152,758,236],{"class":196},[152,760,729],{"class":200},[152,762,763,766,769,771,774],{"class":81,"line":386},[152,764,765],{"class":200},"chart.add_data(data, ",[152,767,768],{"class":492},"titles_from_data",[152,770,236],{"class":196},[152,772,773],{"class":277},"True",[152,775,245],{"class":200},[152,777,779],{"class":81,"line":778},16,[152,780,781],{"class":200},"chart.set_categories(cats)\n",[152,783,785,788,790,793,795],{"class":81,"line":784},17,[152,786,787],{"class":200},"chart.height, chart.width ",[152,789,236],{"class":196},[152,791,792],{"class":277}," 8",[152,794,525],{"class":200},[152,796,797],{"class":277},"15\n",[152,799,801],{"class":81,"line":800},18,[152,802,208],{"emptyLinePlaceholder":207},[152,804,806,809,812],{"class":81,"line":805},19,[152,807,808],{"class":200},"ws.add_chart(chart, ",[152,810,811],{"class":161},"\"E2\"",[152,813,245],{"class":200},[152,815,817,819,821],{"class":81,"line":816},20,[152,818,251],{"class":200},[152,820,572],{"class":161},[152,822,245],{"class":200},[10,824,825,826,23],{},"Keep the chart-building code beside the template so the two stay in step, and treat the template as owning the layout and styling while the code owns the chart. The chart options are covered in ",[19,827,829],{"href":828},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002F","Creating Charts in Excel with openpyxl",[25,831,34,836,34,839,34,842,34,845,34,848,34,855,34,861,34,865,34,869,34,873,34,878,34,881,34,885,34,888,34,891,34,894,34,898,34,903,34,908,34,911,34,914,34,917],{"viewBox":832,"role":28,"ariaLabelledBy":833,"xmlns":32,"style":33},"0 0 760 234",[834,835],"tp2-t","tp2-d",[36,837,838],{"id":834},"Three strategies for filling a template with visuals",[40,840,841],{"id":835},"Write only cells when the objects survive, rebuild the visuals in code when they do not, or drive a real application when the template is too elaborate to reproduce.",[44,843],{"x":46,"y":46,"width":47,"height":844,"fill":49},"234",[51,846,847],{"x":53,"y":54,"style":55},"Cheapest first — try them in this order",[44,849],{"x":850,"y":851,"width":852,"height":853,"rx":62,"fill":854,"stroke":64},"28","46","226","152","#d9f4f1",[51,856,860],{"x":857,"y":858,"style":859},"141","76","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","1. cells only",[51,862,864],{"x":857,"y":863,"style":105},"106","dynamic ranges in",[51,866,868],{"x":857,"y":867,"style":105},"128","the template",[51,870,872],{"x":857,"y":871,"style":74},"154","needs objects to survive",[51,874,877],{"x":857,"y":875,"style":876},"178","font-size:11.5px;fill:var(--teal-ink,#0b6157);text-anchor:middle","simplest by far",[44,879],{"x":880,"y":851,"width":852,"height":853,"rx":62,"fill":97,"stroke":64},"268",[51,882,884],{"x":883,"y":858,"style":101},"381","2. rebuild in code",[51,886,887],{"x":883,"y":863,"style":105},"chart defined in Python",[51,889,890],{"x":883,"y":867,"style":105},"template keeps layout",[51,892,893],{"x":883,"y":871,"style":74},"loses hand tuning",[51,895,897],{"x":883,"y":875,"style":896},"font-size:11.5px;fill:var(--brand-strong,#4338ca);text-anchor:middle","reproducible",[44,899],{"x":900,"y":851,"width":901,"height":853,"rx":62,"fill":902,"stroke":64},"508","224","#fdefd8",[51,904,907],{"x":905,"y":858,"style":906},"620","font-size:12.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","3. drive an app",[51,909,910],{"x":905,"y":863,"style":105},"xlwings or LibreOffice",[51,912,913],{"x":905,"y":867,"style":105},"everything preserved",[51,915,916],{"x":905,"y":871,"style":74},"heavy dependency",[51,918,920],{"x":905,"y":875,"style":919},"font-size:11.5px;fill:var(--gold-ink,#7a4e06);text-anchor:middle","last resort",[137,922,924],{"id":923},"strategy-3-drive-an-application-that-understands-the-file","Strategy 3: drive an application that understands the file",[10,926,927],{},"When the template is too elaborate to reproduce — a formatted dashboard with pivots, slicers and hand-placed shapes — let something that fully understands the format do the writing. xlwings edits through a real Excel:",[142,929,931],{"className":181,"code":930,"language":183,"meta":147,"style":147},"import xlwings as xw\n\nwith xw.App(visible=False, add_book=False) as app:\n    app.display_alerts = False\n    book = app.books.open(\"template.xlsx\")\n    try:\n        sheet = book.sheets[\"Data\"]\n        sheet.range(\"A2\").value = rows        # a list of lists\n        book.api.RefreshAll()\n        book.save(\"filled.xlsx\")\n    finally:\n        book.close()\n",[149,932,933,945,949,981,991,1005,1013,1027,1046,1051,1060,1067],{"__ignoreMap":147},[152,934,935,937,940,942],{"class":81,"line":154},[152,936,197],{"class":196},[152,938,939],{"class":200}," xlwings ",[152,941,298],{"class":196},[152,943,944],{"class":200}," xw\n",[152,946,947],{"class":81,"line":193},[152,948,208],{"emptyLinePlaceholder":207},[152,950,951,954,957,960,962,965,967,970,972,974,976,978],{"class":81,"line":204},[152,952,953],{"class":196},"with",[152,955,956],{"class":200}," xw.App(",[152,958,959],{"class":492},"visible",[152,961,236],{"class":196},[152,963,964],{"class":277},"False",[152,966,525],{"class":200},[152,968,969],{"class":492},"add_book",[152,971,236],{"class":196},[152,973,964],{"class":277},[152,975,358],{"class":200},[152,977,298],{"class":196},[152,979,980],{"class":200}," app:\n",[152,982,983,986,988],{"class":81,"line":211},[152,984,985],{"class":200},"    app.display_alerts ",[152,987,236],{"class":196},[152,989,990],{"class":277}," False\n",[152,992,993,996,998,1001,1003],{"class":81,"line":225},[152,994,995],{"class":200},"    book ",[152,997,236],{"class":196},[152,999,1000],{"class":200}," app.books.open(",[152,1002,242],{"class":161},[152,1004,245],{"class":200},[152,1006,1007,1010],{"class":81,"line":230},[152,1008,1009],{"class":196},"    try",[152,1011,1012],{"class":200},":\n",[152,1014,1015,1018,1020,1023,1025],{"class":81,"line":248},[152,1016,1017],{"class":200},"        sheet ",[152,1019,236],{"class":196},[152,1021,1022],{"class":200}," book.sheets[",[152,1024,467],{"class":161},[152,1026,470],{"class":200},[152,1028,1029,1032,1035,1038,1040,1043],{"class":81,"line":259},[152,1030,1031],{"class":200},"        sheet.range(",[152,1033,1034],{"class":161},"\"A2\"",[152,1036,1037],{"class":200},").value ",[152,1039,236],{"class":196},[152,1041,1042],{"class":200}," rows        ",[152,1044,1045],{"class":648},"# a list of lists\n",[152,1047,1048],{"class":81,"line":264},[152,1049,1050],{"class":200},"        book.api.RefreshAll()\n",[152,1052,1053,1056,1058],{"class":81,"line":289},[152,1054,1055],{"class":200},"        book.save(",[152,1057,572],{"class":161},[152,1059,245],{"class":200},[152,1061,1062,1065],{"class":81,"line":304},[152,1063,1064],{"class":196},"    finally",[152,1066,1012],{"class":200},[152,1068,1069],{"class":81,"line":340},[152,1070,1071],{"class":200},"        book.close()\n",[10,1073,1074,1075,23],{},"Everything is preserved because nothing was rebuilt. The cost is a Windows or macOS machine with Excel and an interactive session, which rules it out for most servers — the trade-offs are set out in ",[19,1076,1078],{"href":1077},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Frefresh-pivot-tables-and-queries-in-excel-with-python\u002F","Refresh Excel pivot tables and queries with Python",[137,1080,1082],{"id":1081},"keep-images-by-re-adding-them","Keep images by re-adding them",[10,1084,1085],{},"An image that does not survive is easy to restore, because it is just a file plus an anchor:",[142,1087,1089],{"className":181,"code":1088,"language":183,"meta":147,"style":147},"from openpyxl.drawing.image import Image\n\nlogo = Image(\"assets\u002Flogo.png\")\nlogo.width, logo.height = 180, 48        # pixels\nws.add_image(logo, \"A1\")\n",[149,1090,1091,1103,1107,1122,1140],{"__ignoreMap":147},[152,1092,1093,1095,1098,1100],{"class":81,"line":154},[152,1094,214],{"class":196},[152,1096,1097],{"class":200}," openpyxl.drawing.image ",[152,1099,197],{"class":196},[152,1101,1102],{"class":200}," Image\n",[152,1104,1105],{"class":81,"line":193},[152,1106,208],{"emptyLinePlaceholder":207},[152,1108,1109,1112,1114,1117,1120],{"class":81,"line":204},[152,1110,1111],{"class":200},"logo ",[152,1113,236],{"class":196},[152,1115,1116],{"class":200}," Image(",[152,1118,1119],{"class":161},"\"assets\u002Flogo.png\"",[152,1121,245],{"class":200},[152,1123,1124,1127,1129,1132,1134,1137],{"class":81,"line":211},[152,1125,1126],{"class":200},"logo.width, logo.height ",[152,1128,236],{"class":196},[152,1130,1131],{"class":277}," 180",[152,1133,525],{"class":200},[152,1135,1136],{"class":277},"48",[152,1138,1139],{"class":648},"        # pixels\n",[152,1141,1142,1145,1148],{"class":81,"line":225},[152,1143,1144],{"class":200},"ws.add_image(logo, ",[152,1146,1147],{"class":161},"\"A1\"",[152,1149,245],{"class":200},[10,1151,1152,1153,23],{},"Keep the source image beside the template in version control rather than extracting it from the workbook each run — the extraction is fragile and the file is small. Placement details are in ",[19,1154,1156],{"href":1155},"\u002Fformatting-and-charting-excel-reports-with-python\u002Finserting-images-and-logos-into-excel\u002Fadd-logo-image-to-excel-report-with-openpyxl\u002F","Add a logo image to an Excel report with openpyxl",[137,1158,1160],{"id":1159},"consider-building-the-whole-workbook-instead","Consider building the whole workbook instead",[10,1162,1163,1164,23],{},"Templates exist so a non-developer can own the design. When that is not actually true — when the template is a file someone made once and nobody maintains — building from code with xlsxwriter is more robust: nothing to preserve, nothing to lose, and the layout is in version control with the rest of the job. The reasonable rule is to keep a template when a business owner genuinely edits it, and to generate from code otherwise. The generation route is covered in ",[19,1165,1167],{"href":1166},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-a-formatted-excel-report-with-xlsxwriter\u002F","Write a formatted Excel report with xlsxwriter",[137,1169,1171],{"id":1170},"check-the-output-before-delivering-it","Check the output before delivering it",[10,1173,1174],{},"Whichever strategy you choose, assert that the visuals are present rather than trusting they are:",[142,1176,1178],{"className":181,"code":1177,"language":183,"meta":147,"style":147},"import zipfile\n\ndef assert_visuals(path: str, expect_charts: int = 1) -> None:\n    with zipfile.ZipFile(path) as zf:\n        charts = [n for n in zf.namelist() if n.startswith(\"xl\u002Fcharts\u002Fchart\")]\n        drawings = [n for n in zf.namelist() if n.startswith(\"xl\u002Fdrawings\u002Fdrawing\")]\n    if len(charts) \u003C expect_charts:\n        raise ValueError(f\"{path}: expected {expect_charts} chart(s), found {len(charts)}\")\n    print(f\"{path}: {len(charts)} chart(s), {len(drawings)} drawing(s)\")\n\nassert_visuals(\"filled.xlsx\", expect_charts=1)\n",[149,1179,1180,1186,1190,1221,1231,1259,1285,1302,1355,1399,1403],{"__ignoreMap":147},[152,1181,1182,1184],{"class":81,"line":154},[152,1183,197],{"class":196},[152,1185,201],{"class":200},[152,1187,1188],{"class":81,"line":193},[152,1189,208],{"emptyLinePlaceholder":207},[152,1191,1192,1194,1197,1199,1201,1204,1207,1210,1213,1216,1219],{"class":81,"line":204},[152,1193,267],{"class":196},[152,1195,1196],{"class":270}," assert_visuals",[152,1198,274],{"class":200},[152,1200,278],{"class":277},[152,1202,1203],{"class":200},", expect_charts: ",[152,1205,1206],{"class":277},"int",[152,1208,1209],{"class":196}," =",[152,1211,1212],{"class":277}," 1",[152,1214,1215],{"class":200},") -> ",[152,1217,1218],{"class":277},"None",[152,1220,1012],{"class":200},[152,1222,1223,1225,1227,1229],{"class":81,"line":211},[152,1224,292],{"class":196},[152,1226,295],{"class":200},[152,1228,298],{"class":196},[152,1230,301],{"class":200},[152,1232,1233,1236,1238,1241,1243,1245,1247,1249,1251,1253,1256],{"class":81,"line":225},[152,1234,1235],{"class":200},"        charts ",[152,1237,236],{"class":196},[152,1239,1240],{"class":200}," [n ",[152,1242,313],{"class":196},[152,1244,316],{"class":200},[152,1246,319],{"class":196},[152,1248,322],{"class":200},[152,1250,325],{"class":196},[152,1252,331],{"class":200},[152,1254,1255],{"class":161},"\"xl\u002Fcharts\u002Fchart\"",[152,1257,1258],{"class":200},")]\n",[152,1260,1261,1264,1266,1268,1270,1272,1274,1276,1278,1280,1283],{"class":81,"line":230},[152,1262,1263],{"class":200},"        drawings ",[152,1265,236],{"class":196},[152,1267,1240],{"class":200},[152,1269,313],{"class":196},[152,1271,316],{"class":200},[152,1273,319],{"class":196},[152,1275,322],{"class":200},[152,1277,325],{"class":196},[152,1279,331],{"class":200},[152,1281,1282],{"class":161},"\"xl\u002Fdrawings\u002Fdrawing\"",[152,1284,1258],{"class":200},[152,1286,1287,1290,1293,1296,1299],{"class":81,"line":248},[152,1288,1289],{"class":196},"    if",[152,1291,1292],{"class":277}," len",[152,1294,1295],{"class":200},"(charts) ",[152,1297,1298],{"class":196},"\u003C",[152,1300,1301],{"class":200}," expect_charts:\n",[152,1303,1304,1307,1310,1312,1315,1318,1322,1325,1328,1331,1333,1336,1338,1341,1343,1346,1349,1351,1353],{"class":81,"line":259},[152,1305,1306],{"class":196},"        raise",[152,1308,1309],{"class":277}," ValueError",[152,1311,392],{"class":200},[152,1313,1314],{"class":196},"f",[152,1316,1317],{"class":161},"\"",[152,1319,1321],{"class":1320},"sSjpA","{",[152,1323,1324],{"class":200},"path",[152,1326,1327],{"class":1320},"}",[152,1329,1330],{"class":161},": expected ",[152,1332,1321],{"class":1320},[152,1334,1335],{"class":200},"expect_charts",[152,1337,1327],{"class":1320},[152,1339,1340],{"class":161}," chart(s), found ",[152,1342,1321],{"class":1320},[152,1344,1345],{"class":277},"len",[152,1347,1348],{"class":200},"(charts)",[152,1350,1327],{"class":1320},[152,1352,1317],{"class":161},[152,1354,245],{"class":200},[152,1356,1357,1359,1361,1363,1365,1367,1369,1371,1374,1376,1378,1380,1382,1385,1387,1389,1392,1394,1397],{"class":81,"line":264},[152,1358,389],{"class":277},[152,1360,392],{"class":200},[152,1362,1314],{"class":196},[152,1364,1317],{"class":161},[152,1366,1321],{"class":1320},[152,1368,1324],{"class":200},[152,1370,1327],{"class":1320},[152,1372,1373],{"class":161},": ",[152,1375,1321],{"class":1320},[152,1377,1345],{"class":277},[152,1379,1348],{"class":200},[152,1381,1327],{"class":1320},[152,1383,1384],{"class":161}," chart(s), ",[152,1386,1321],{"class":1320},[152,1388,1345],{"class":277},[152,1390,1391],{"class":200},"(drawings)",[152,1393,1327],{"class":1320},[152,1395,1396],{"class":161}," drawing(s)\"",[152,1398,245],{"class":200},[152,1400,1401],{"class":81,"line":289},[152,1402,208],{"emptyLinePlaceholder":207},[152,1404,1405,1408,1410,1412,1414,1416,1418],{"class":81,"line":304},[152,1406,1407],{"class":200},"assert_visuals(",[152,1409,572],{"class":161},[152,1411,525],{"class":200},[152,1413,1335],{"class":492},[152,1415,236],{"class":196},[152,1417,522],{"class":277},[152,1419,245],{"class":200},[10,1421,1422,1423,23],{},"Add it to the pre-delivery checks so a library upgrade that changes what survives is caught by the job rather than by the recipient — the same idea as ",[19,1424,1426],{"href":1425},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fvalidate-an-excel-report-before-sending-it\u002F","Validate an Excel report before sending it",[137,1428,1430],{"id":1429},"split-the-template-into-a-data-sheet-and-a-presentation-sheet","Split the template into a data sheet and a presentation sheet",[10,1432,1433,1434,1437,1438,1441,1442,1444],{},"The structural fix that makes strategy 1 work reliably is to stop mixing the two concerns. Give the template a plain ",[149,1435,1436],{},"Data"," sheet that code writes to, and a ",[149,1439,1440],{},"Report"," sheet that holds every chart, formula and piece of formatting, referencing ",[149,1443,1436],{}," by range. Code then touches only cells on a sheet with nothing to lose.",[142,1446,1448],{"className":181,"code":1447,"language":183,"meta":147,"style":147},"from openpyxl import load_workbook\n\nwb = load_workbook(\"template.xlsx\")\ndata = wb[\"Data\"]\n\n# clear last run's rows without touching anything else\nif data.max_row > 1:\n    data.delete_rows(2, data.max_row - 1)\n\nfor r, (region, revenue) in enumerate(rows, start=2):\n    data.cell(row=r, column=1, value=region)\n    data.cell(row=r, column=2, value=float(revenue))\n\nwb.save(\"filled.xlsx\")\n",[149,1449,1450,1460,1464,1476,1488,1492,1497,1511,1527,1531,1551,1576,1602,1606],{"__ignoreMap":147},[152,1451,1452,1454,1456,1458],{"class":81,"line":154},[152,1453,214],{"class":196},[152,1455,217],{"class":200},[152,1457,197],{"class":196},[152,1459,222],{"class":200},[152,1461,1462],{"class":81,"line":193},[152,1463,208],{"emptyLinePlaceholder":207},[152,1465,1466,1468,1470,1472,1474],{"class":81,"line":204},[152,1467,233],{"class":200},[152,1469,236],{"class":196},[152,1471,239],{"class":200},[152,1473,242],{"class":161},[152,1475,245],{"class":200},[152,1477,1478,1480,1482,1484,1486],{"class":81,"line":211},[152,1479,698],{"class":200},[152,1481,236],{"class":196},[152,1483,464],{"class":200},[152,1485,467],{"class":161},[152,1487,470],{"class":200},[152,1489,1490],{"class":81,"line":225},[152,1491,208],{"emptyLinePlaceholder":207},[152,1493,1494],{"class":81,"line":230},[152,1495,1496],{"class":648},"# clear last run's rows without touching anything else\n",[152,1498,1499,1501,1504,1507,1509],{"class":81,"line":248},[152,1500,325],{"class":196},[152,1502,1503],{"class":200}," data.max_row ",[152,1505,1506],{"class":196},">",[152,1508,1212],{"class":277},[152,1510,1012],{"class":200},[152,1512,1513,1516,1518,1521,1523,1525],{"class":81,"line":259},[152,1514,1515],{"class":200},"    data.delete_rows(",[152,1517,498],{"class":277},[152,1519,1520],{"class":200},", data.max_row ",[152,1522,361],{"class":196},[152,1524,1212],{"class":277},[152,1526,245],{"class":200},[152,1528,1529],{"class":81,"line":264},[152,1530,208],{"emptyLinePlaceholder":207},[152,1532,1533,1535,1537,1539,1541,1543,1545,1547,1549],{"class":81,"line":289},[152,1534,313],{"class":196},[152,1536,481],{"class":200},[152,1538,319],{"class":196},[152,1540,486],{"class":277},[152,1542,489],{"class":200},[152,1544,493],{"class":492},[152,1546,236],{"class":196},[152,1548,498],{"class":277},[152,1550,501],{"class":200},[152,1552,1553,1556,1558,1560,1562,1564,1566,1568,1570,1572,1574],{"class":81,"line":304},[152,1554,1555],{"class":200},"    data.cell(",[152,1557,509],{"class":492},[152,1559,236],{"class":196},[152,1561,514],{"class":200},[152,1563,517],{"class":492},[152,1565,236],{"class":196},[152,1567,522],{"class":277},[152,1569,525],{"class":200},[152,1571,528],{"class":492},[152,1573,236],{"class":196},[152,1575,533],{"class":200},[152,1577,1578,1580,1582,1584,1586,1588,1590,1592,1594,1596,1598,1600],{"class":81,"line":340},[152,1579,1555],{"class":200},[152,1581,509],{"class":492},[152,1583,236],{"class":196},[152,1585,514],{"class":200},[152,1587,517],{"class":492},[152,1589,236],{"class":196},[152,1591,498],{"class":277},[152,1593,525],{"class":200},[152,1595,528],{"class":492},[152,1597,236],{"class":196},[152,1599,558],{"class":277},[152,1601,561],{"class":200},[152,1603,1604],{"class":81,"line":345},[152,1605,208],{"emptyLinePlaceholder":207},[152,1607,1608,1610,1612],{"class":81,"line":370},[152,1609,251],{"class":200},[152,1611,572],{"class":161},[152,1613,245],{"class":200},[10,1615,1616,1619],{},[149,1617,1618],{},"delete_rows"," keeps the sheet clean between runs, so a month with fewer rows does not leave last month's tail behind the new data — a subtle bug that shows up as a chart with a flat, wrong final segment.",[25,1621,34,1626,34,1629,34,1632,34,1638,34,1641,34,1645,34,1649,34,1653,34,1657,34,1663,34,1667,34,1672,34,1675,34,1679,34,1682,34,1685,34,1689,34,1695,34,1699,34,1704],{"viewBox":1622,"role":28,"ariaLabelledBy":1623,"xmlns":32,"style":33},"18 -3 724 175",[1624,1625],"tp3-t","tp3-d",[36,1627,1628],{"id":1624},"Separating the sheet code writes from the sheet people see",[40,1630,1631],{"id":1625},"Code writes plain values to a data sheet, while the presentation sheet holds the charts and formatting and references the data sheet by range.",[44,1633],{"x":1634,"y":1635,"width":1636,"height":1637,"fill":49},"18","-3","724","175",[51,1639,1640],{"x":53,"y":54,"style":55},"Nothing fragile lives where code writes",[44,1642],{"x":1643,"y":1644,"width":48,"height":68,"rx":62,"fill":97,"stroke":64},"34","60",[51,1646,1648],{"x":871,"y":1647,"style":101},"94","Data sheet",[51,1650,1652],{"x":871,"y":1651,"style":105},"120","values only, written by code",[51,1654,1656],{"x":871,"y":1655,"style":74},"142","safe to clear and refill",[81,1658],{"x1":1659,"y1":1660,"x2":1661,"y2":1660,"stroke":1662,"style":87},"274","108","324","var(--teal,#0f9488)",[89,1664],{"points":1665,"fill":1666},"324,108 314,103 314,113","#0f766e",[51,1668,1671],{"x":1669,"y":68,"style":1670},"299","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","refs",[44,1673],{"x":1674,"y":1644,"width":48,"height":68,"rx":62,"fill":854,"stroke":64},"328",[51,1676,1678],{"x":1677,"y":1647,"style":859},"448","Report sheet",[51,1680,1681],{"x":1677,"y":1651,"style":105},"charts, formulas, styling",[51,1683,1684],{"x":1677,"y":1655,"style":876},"code never writes here",[44,1686],{"x":1687,"y":1644,"width":1688,"height":68,"rx":62,"fill":902,"stroke":64},"600","126",[51,1690,1694],{"x":1691,"y":1692,"style":1693},"663","100","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","owner edits",[51,1696,1698],{"x":1691,"y":1697,"style":1693},"124","this one",[81,1700],{"x1":1701,"y1":1660,"x2":1702,"y2":1660,"stroke":1703,"style":87},"568","596","var(--gold,#b4740a)",[89,1705],{"points":1706,"fill":1707},"596,108 586,103 586,113","#7a4e06",[10,1709,1710],{},"That separation also settles ownership: the business owner edits the report sheet freely, and the only contract between them and the code is the column layout of the data sheet.",[137,1712,1714],{"id":1713},"common-pitfalls-and-gotchas","Common pitfalls and gotchas",[1716,1717,1718,1726,1739,1745,1751],"ul",{},[1719,1720,1721,1725],"li",{},[1722,1723,1724],"strong",{},"Assuming an error would have told you."," The loss is silent; only a check catches it.",[1719,1727,1728,1734,1735,1738],{},[1722,1729,1730,1733],{},[149,1731,1732],{},"keep_vba"," as a fix."," It preserves macros in ",[149,1736,1737],{},".xlsm",", not charts or pivots.",[1719,1740,1741,1744],{},[1722,1742,1743],{},"Renaming a sheet the chart references."," That breaks the chart even when the object survives.",[1719,1746,1747,1750],{},[1722,1748,1749],{},"Writing over the template."," Always write to a new path, so a bad run does not destroy the source.",[1719,1752,1753,1756],{},[1722,1754,1755],{},"A chart range that does not grow."," Filling more rows than the template's fixed range leaves the extra data out of the chart.",[137,1758,1760],{"id":1759},"performance-and-scale-notes","Performance and scale notes",[10,1762,1763,1764,23],{},"Strategies 1 and 2 are pure Python and run in milliseconds per report, which matters when generating one workbook per region in a loop. Strategy 3 costs an application start of seconds and serialises — one Excel instance at a time — so a hundred reports become a batch job rather than a loop. That difference is usually decisive: if the report set is large, invest in reproducing the template in code once rather than paying for an application launch on every run. Where a template must be filled per recipient, the loop pattern is in ",[19,1765,1767],{"href":1766},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-excel-report-per-region-in-a-loop\u002F","Generate one Excel report per region in a loop",[137,1769,1771],{"id":1770},"conclusion","Conclusion",[10,1773,1774],{},"openpyxl rewrites the workbook rather than editing it, so anything outside its object model disappears on save. Diff the zip parts to learn what your template actually loses, then pick the cheapest strategy that works: write only cells when the visuals survive, rebuild charts in code when they do not, and drive Excel only when the template is genuinely too elaborate to reproduce. Whichever you choose, assert that the charts and drawings are present in the output before it goes out.",[137,1776,1778],{"id":1777},"frequently-asked-questions","Frequently asked questions",[10,1780,1781,1784],{},[1722,1782,1783],{},"Why does openpyxl delete my chart?","\nopenpyxl reads a workbook into its own object model and writes a new file from it. Anything it does not model — most charts it did not create, some images, pivot tables, slicers, form controls — has nothing to write back and disappears.",[10,1786,1787,1790],{},[1722,1788,1789],{},"Which objects survive a round trip?","\nCell values, formulas, most styles, conditional formatting, merged cells, defined names and print settings generally survive. Charts, pivot caches, slicers, form controls and some drawings often do not.",[10,1792,1793,1796],{},[1722,1794,1795],{},"What is the safest way to fill a template with a chart?","\nPoint the chart at a dynamic range and write only the data cells, then have Excel redraw the chart on open. Where a chart must be redrawn server-side, rebuild it with openpyxl after filling.",[10,1798,1799,1802,1803,1805],{},[1722,1800,1801],{},"Does keep_vba help?","\nIt preserves macros in an ",[149,1804,1737],{},", which is a different problem. It does not preserve charts or pivot tables.",[10,1807,1808,1811],{},[1722,1809,1810],{},"Can I avoid templates entirely?","\nOften yes, and it is usually the more robust choice — build the whole workbook from code with xlsxwriter, so nothing depends on preserving objects a library cannot model.",[137,1813,1815],{"id":1814},"related","Related",[1716,1817,1818,1827,1834,1841,1846],{},[1719,1819,1820,1823,1824,1826],{},[1722,1821,1822],{},"Up:"," ",[19,1825,22],{"href":21}," — the template workflow this problem interrupts.",[1719,1828,1829,1833],{},[19,1830,1832],{"href":1831},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","Populate an Excel template without losing formatting"," — the styles side of the same round trip.",[1719,1835,1836,1840],{},[19,1837,1839],{"href":1838},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Ffill-excel-template-with-python-openpyxl\u002F","Fill an Excel template with Python and openpyxl"," — the basic fill this page hardens.",[1719,1842,1843,1845],{},[19,1844,829],{"href":828}," — rebuilding the chart the template lost.",[1719,1847,1848,1852],{},[19,1849,1851],{"href":1850},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fwork-with-macro-enabled-xlsm-files-in-openpyxl\u002F","Work with macro-enabled xlsm files in openpyxl"," — the related preservation problem for VBA.",[1854,1855,1856],"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 .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":147,"searchDepth":193,"depth":193,"links":1858},[1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872],{"id":139,"depth":193,"text":140},{"id":174,"depth":193,"text":175},{"id":411,"depth":193,"text":412},{"id":580,"depth":193,"text":581},{"id":923,"depth":193,"text":924},{"id":1081,"depth":193,"text":1082},{"id":1159,"depth":193,"text":1160},{"id":1170,"depth":193,"text":1171},{"id":1429,"depth":193,"text":1430},{"id":1713,"depth":193,"text":1714},{"id":1759,"depth":193,"text":1760},{"id":1770,"depth":193,"text":1771},{"id":1777,"depth":193,"text":1778},{"id":1814,"depth":193,"text":1815},"2026-08-27","openpyxl drops charts, images and pivot tables on re-save. Why it happens, which objects survive, and three ways to fill a template without losing its visuals.","md",[1877,1879,1881,1883,1885],{"q":1783,"a":1878},"openpyxl reads a workbook into its own object model and writes a new file from it. Anything it does not model — most chart types it did not create, some images, pivot tables, slicers, form controls — has nothing to write back and disappears.",{"q":1789,"a":1880},"Cell values, formulas, most styles, conditional formatting, merged cells, defined names and print settings generally survive. Charts, pivot caches, slicers, form controls and some drawings often do not.",{"q":1795,"a":1882},"Point the chart at a dynamic range and write only the data cells, then have Excel redraw the chart on open. Where a chart must be redrawn server-side, rebuild it with openpyxl after filling.",{"q":1801,"a":1884},"It preserves macros in an .xlsm, which is a different problem. It does not preserve charts or pivot tables.",{"q":1810,"a":1886},"Often yes, and it is usually the more robust choice — build the whole workbook from code with xlsxwriter, so nothing depends on preserving objects a library cannot model.",{},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template",{"title":1890,"description":1891},"Keep Charts When Filling an Excel Template","Stop openpyxl losing charts, images and pivots when populating a template: keep_vba and keep_links, dynamic named ranges, rebuilding charts, or LibreOffice and xlwings.","keep-charts-and-images-when-filling-an-excel-template","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template\u002Findex","how-to","CUUrwo3LtEr-hjWadrGpIwLr0PwcxgM3TxYhGGKUMug",[1897,1901],{"title":1898,"path":1899,"stem":1900,"children":-1},"Generate One Excel Report per Region in a Loop","\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-excel-report-per-region-in-a-loop","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-excel-report-per-region-in-a-loop\u002Findex",{"title":1902,"path":1903,"stem":1904,"children":-1},"Populate an Excel Template Without Losing Formatting","\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002Findex",1788710163061]