[{"data":1,"prerenderedAt":2975},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Freplace-placeholders-in-an-excel-template-with-python":3,"surround:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Freplace-placeholders-in-an-excel-template-with-python":2938,"ancestors:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Freplace-placeholders-in-an-excel-template-with-python":2947,"children:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Freplace-placeholders-in-an-excel-template-with-python":2953,"related:\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Freplace-placeholders-in-an-excel-template-with-python":2954},{"id":4,"title":5,"body":6,"dateModified":2906,"datePublished":2906,"description":2907,"extension":2908,"faq":2909,"keyword":2918,"kind":2919,"meta":2920,"navigation":340,"path":2929,"seo":2930,"slug":2932,"stem":2933,"topics":2934,"type":2936,"__hash__":2937},"docs\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Freplace-placeholders-in-an-excel-template-with-python\u002Findex.md","Replace Placeholders in an Excel Template with Python",{"type":7,"value":8,"toc":2893},"minimark",[9,32,266,271,296,299,792,796,803,1103,1117,1121,1133,1554,1564,1634,1638,1645,2205,2222,2226,2229,2379,2382,2470,2474,2481,2685,2691,2695,2796,2800,2811,2815,2818,2822,2829,2835,2841,2847,2851,2889],[10,11,12,13,17,18,21,22,25,26,31],"p",{},"Filling a template by cell address — \"the customer name goes in ",[14,15,16],"code",{},"C4","\" — works until somebody inserts a row above\nthe header and every address in the script is off by one. Placeholders move the contract into the template itself.\nThe person who designs the report types ",[14,19,20],{},"{{customer}}"," where the customer name should appear and\n",[14,23,24],{},"Invoice for {{period}}"," where a sentence needs a value, formats those cells however they like, and your script\nfinds and replaces the markers wherever they currently are. This guide, part of\n",[27,28,30],"a",{"href":29},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002F","Generating Excel Reports from Templates",",\nbuilds a placeholder filler with openpyxl.",[33,34,42,43,42,47,42,51,42,58,42,66,42,70,42,77,42,81,42,85,42,88,42,92,42,95,42,100,42,104,42,108,42,110,42,112,42,115,42,119,42,121,42,125,42,129,42,132,42,134,42,141,42,146,42,148,42,152,42,154,42,157,42,159,42,162,42,164,42,167,42,171,42,174,42,178,42,180,42,183,42,187,42,191,42,193,42,196,42,200,42,202,42,204,42,206,42,208,42,212,42,214,42,218,42,220,42,223,42,225,42,228,42,231,42,235,42,239,42,243,42,245,42,248,42,250,42,253,42,256,42,260],"svg",{"viewBox":35,"role":36,"ariaLabelledBy":37,"xmlns":40,"style":41},"0 0 666 314","img",[38,39],"ph-template-t","ph-template-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:666px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[44,45,46],"title",{"id":38},"A template with placeholders in cells",[48,49,50],"desc",{"id":39},"An invoice template where B2 holds the customer placeholder, B3 a sentence with a period placeholder, B4 a total placeholder formatted as currency, and row 7 a repeating line-item row with item, quantity and price placeholders.",[52,53],"rect",{"x":54,"y":54,"width":55,"height":56,"fill":57},"0","666","314","#ffffff",[52,59],{"x":60,"y":60,"width":61,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},"20","36","30","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[52,67],{"x":68,"y":60,"width":69,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},"56","110",[71,72,76],"text",{"x":73,"y":74,"style":75},"111","40","font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[52,78],{"x":79,"y":60,"width":80,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},"166","200",[71,82,84],{"x":83,"y":74,"style":75},"266","B",[52,86],{"x":87,"y":60,"width":69,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},"366",[71,89,91],{"x":90,"y":74,"style":75},"421","C",[52,93],{"x":60,"y":94,"width":61,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},"50",[71,96,99],{"x":97,"y":98,"style":75},"38","70","1",[52,101],{"x":68,"y":94,"width":69,"height":62,"rx":54,"fill":102,"stroke":103,"style":65},"#f0f4ff","var(--brand,#5b5cf0)",[71,105,107],{"x":73,"y":98,"style":106},"font-size:11.5px;font-weight:600;fill:var(--brand-strong,#4338ca);text-anchor:middle","INVOICE",[52,109],{"x":79,"y":94,"width":80,"height":62,"rx":54,"fill":57,"stroke":64,"style":65},[52,111],{"x":87,"y":94,"width":69,"height":62,"rx":54,"fill":57,"stroke":64,"style":65},[52,113],{"x":60,"y":114,"width":61,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},"80",[71,116,118],{"x":97,"y":117,"style":75},"100","2",[52,120],{"x":68,"y":114,"width":69,"height":62,"rx":54,"fill":57,"stroke":64,"style":65},[71,122,124],{"x":73,"y":117,"style":123},"font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Customer",[52,126],{"x":79,"y":114,"width":80,"height":62,"rx":54,"fill":127,"stroke":128,"style":65},"#d9f4f1","var(--teal,#0f9488)",[71,130,20],{"x":83,"y":117,"style":131},"font-size:11.5px;font-weight:600;fill:var(--teal-ink,#0b6157);text-anchor:middle",[52,133],{"x":87,"y":114,"width":69,"height":62,"rx":54,"fill":57,"stroke":64,"style":65},[52,135],{"x":136,"y":137,"width":138,"height":139,"rx":140,"fill":127,"stroke":128,"style":65},"496","82","150","26","8",[71,142,145],{"x":143,"y":144,"style":131},"571","99","whole-cell value",[52,147],{"x":60,"y":69,"width":61,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},[71,149,151],{"x":97,"y":150,"style":75},"130","3",[52,153],{"x":68,"y":69,"width":69,"height":62,"rx":54,"fill":57,"stroke":64,"style":65},[71,155,156],{"x":73,"y":150,"style":123},"Period",[52,158],{"x":79,"y":69,"width":80,"height":62,"rx":54,"fill":127,"stroke":128,"style":65},[71,160,161],{"x":83,"y":150,"style":131},"Services for {{period}}",[52,163],{"x":87,"y":69,"width":69,"height":62,"rx":54,"fill":57,"stroke":64,"style":65},[52,165],{"x":136,"y":166,"width":138,"height":139,"rx":140,"fill":127,"stroke":128,"style":65},"112",[71,168,170],{"x":143,"y":169,"style":131},"129","inside a sentence",[52,172],{"x":60,"y":173,"width":61,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},"140",[71,175,177],{"x":97,"y":176,"style":75},"160","4",[52,179],{"x":68,"y":173,"width":69,"height":62,"rx":54,"fill":57,"stroke":64,"style":65},[71,181,182],{"x":73,"y":176,"style":123},"Total due",[52,184],{"x":79,"y":173,"width":80,"height":62,"rx":54,"fill":185,"stroke":186,"style":65},"#fdefd8","var(--gold,#b4740a)",[71,188,190],{"x":83,"y":176,"style":189},"font-size:11.5px;font-weight:600;fill:var(--gold-ink,#7a4e06);text-anchor:middle","{{total}}",[52,192],{"x":87,"y":173,"width":69,"height":62,"rx":54,"fill":57,"stroke":64,"style":65},[52,194],{"x":60,"y":195,"width":61,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},"170",[71,197,199],{"x":97,"y":198,"style":75},"190","5",[52,201],{"x":68,"y":195,"width":69,"height":62,"rx":54,"fill":57,"stroke":64,"style":65},[52,203],{"x":79,"y":195,"width":80,"height":62,"rx":54,"fill":57,"stroke":64,"style":65},[52,205],{"x":87,"y":195,"width":69,"height":62,"rx":54,"fill":57,"stroke":64,"style":65},[52,207],{"x":60,"y":80,"width":61,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},[71,209,211],{"x":97,"y":210,"style":75},"220","6",[52,213],{"x":68,"y":80,"width":69,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},[71,215,217],{"x":73,"y":210,"style":216},"font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","Item",[52,219],{"x":79,"y":80,"width":80,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},[71,221,222],{"x":83,"y":210,"style":216},"Quantity",[52,224],{"x":87,"y":80,"width":69,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},[71,226,227],{"x":90,"y":210,"style":216},"Price",[52,229],{"x":60,"y":230,"width":61,"height":62,"rx":54,"fill":63,"stroke":64,"style":65},"230",[71,232,234],{"x":97,"y":233,"style":75},"250","7",[52,236],{"x":68,"y":230,"width":69,"height":62,"rx":54,"fill":237,"stroke":238,"style":65},"#fee8f2","var(--accent,#f43f8f)",[71,240,242],{"x":73,"y":233,"style":241},"font-size:11.5px;font-weight:600;fill:var(--accent-ink,#be185d);text-anchor:middle","{{item.name}}",[52,244],{"x":79,"y":230,"width":80,"height":62,"rx":54,"fill":237,"stroke":238,"style":65},[71,246,247],{"x":83,"y":233,"style":241},"{{item.qty}}",[52,249],{"x":87,"y":230,"width":69,"height":62,"rx":54,"fill":237,"stroke":238,"style":65},[71,251,252],{"x":90,"y":233,"style":241},"{{item.price}}",[52,254],{"x":136,"y":255,"width":138,"height":139,"rx":140,"fill":237,"stroke":238,"style":65},"232",[71,257,259],{"x":143,"y":258,"style":241},"249","repeats per item",[71,261,265],{"x":262,"y":263,"style":264},"333","292","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","the template carries the layout; the markers carry the contract",[267,268,270],"h2",{"id":269},"prerequisites","Prerequisites",[272,273,278],"pre",{"className":274,"code":275,"language":276,"meta":277,"style":277},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[14,279,280],{"__ignoreMap":277},[281,282,285,289,293],"span",{"class":283,"line":284},"line",1,[281,286,288],{"class":287},"sMTad","pip",[281,290,292],{"class":291},"srMev"," install",[281,294,295],{"class":291}," openpyxl\n",[10,297,298],{},"This builds the template shown above, with formatting on the placeholder cells so you can see it survive the fill:",[272,300,304],{"className":301,"code":302,"language":303,"meta":277,"style":277},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import Workbook\nfrom openpyxl.styles import Font, PatternFill\n\nwb = Workbook()\nws = wb.active\nws.title = \"Invoice\"\nws[\"A1\"] = \"INVOICE\"\nws[\"A1\"].font = Font(size=18, bold=True)\nws[\"A2\"], ws[\"B2\"] = \"Customer\", \"{{customer}}\"\nws[\"A3\"], ws[\"B3\"] = \"Period\", \"Services for {{period}}\"\nws[\"A4\"], ws[\"B4\"] = \"Total due\", \"{{total}}\"\nws[\"B4\"].number_format = '\"€\"#,##0.00'\nws[\"B4\"].font = Font(bold=True)\nfor col, label in zip(\"ABC\", [\"Item\", \"Quantity\", \"Price\"]):\n    ws[f\"{col}6\"] = label\n    ws[f\"{col}6\"].fill = PatternFill(\"solid\", start_color=\"E7EBEF\")\nws[\"A7\"], ws[\"B7\"], ws[\"C7\"] = \"{{item.name}}\", \"{{item.qty}}\", \"{{item.price}}\"\nws[\"C7\"].number_format = '\"€\"#,##0.00'\nws[\"A9\"] = \"Thank you for your business, {{customer}}.\"\nwb.save(\"invoice_template.xlsx\")\n","python",[14,305,306,322,335,342,354,365,376,393,432,471,505,538,553,574,614,644,684,744,757,781],{"__ignoreMap":277},[281,307,308,312,316,319],{"class":283,"line":284},[281,309,311],{"class":310},"s-kum","from",[281,313,315],{"class":314},"skGVy"," openpyxl ",[281,317,318],{"class":310},"import",[281,320,321],{"class":314}," Workbook\n",[281,323,325,327,330,332],{"class":283,"line":324},2,[281,326,311],{"class":310},[281,328,329],{"class":314}," openpyxl.styles ",[281,331,318],{"class":310},[281,333,334],{"class":314}," Font, PatternFill\n",[281,336,338],{"class":283,"line":337},3,[281,339,341],{"emptyLinePlaceholder":340},true,"\n",[281,343,345,348,351],{"class":283,"line":344},4,[281,346,347],{"class":314},"wb ",[281,349,350],{"class":310},"=",[281,352,353],{"class":314}," Workbook()\n",[281,355,357,360,362],{"class":283,"line":356},5,[281,358,359],{"class":314},"ws ",[281,361,350],{"class":310},[281,363,364],{"class":314}," wb.active\n",[281,366,368,371,373],{"class":283,"line":367},6,[281,369,370],{"class":314},"ws.title ",[281,372,350],{"class":310},[281,374,375],{"class":291}," \"Invoice\"\n",[281,377,379,382,385,388,390],{"class":283,"line":378},7,[281,380,381],{"class":314},"ws[",[281,383,384],{"class":291},"\"A1\"",[281,386,387],{"class":314},"] ",[281,389,350],{"class":310},[281,391,392],{"class":291}," \"INVOICE\"\n",[281,394,396,398,400,403,405,408,412,414,418,421,424,426,429],{"class":283,"line":395},8,[281,397,381],{"class":314},[281,399,384],{"class":291},[281,401,402],{"class":314},"].font ",[281,404,350],{"class":310},[281,406,407],{"class":314}," Font(",[281,409,411],{"class":410},"sa561","size",[281,413,350],{"class":310},[281,415,417],{"class":416},"sP0c6","18",[281,419,420],{"class":314},", ",[281,422,423],{"class":410},"bold",[281,425,350],{"class":310},[281,427,428],{"class":416},"True",[281,430,431],{"class":314},")\n",[281,433,435,437,440,443,446,448,450,453,455,458,462,465,468],{"class":283,"line":434},9,[281,436,381],{"class":314},[281,438,439],{"class":291},"\"A2\"",[281,441,442],{"class":314},"], ws[",[281,444,445],{"class":291},"\"B2\"",[281,447,387],{"class":314},[281,449,350],{"class":310},[281,451,452],{"class":291}," \"Customer\"",[281,454,420],{"class":314},[281,456,457],{"class":291},"\"",[281,459,461],{"class":460},"sSjpA","{{",[281,463,464],{"class":291},"customer",[281,466,467],{"class":460},"}}",[281,469,470],{"class":291},"\"\n",[281,472,474,476,479,481,484,486,488,491,493,496,498,501,503],{"class":283,"line":473},10,[281,475,381],{"class":314},[281,477,478],{"class":291},"\"A3\"",[281,480,442],{"class":314},[281,482,483],{"class":291},"\"B3\"",[281,485,387],{"class":314},[281,487,350],{"class":310},[281,489,490],{"class":291}," \"Period\"",[281,492,420],{"class":314},[281,494,495],{"class":291},"\"Services for ",[281,497,461],{"class":460},[281,499,500],{"class":291},"period",[281,502,467],{"class":460},[281,504,470],{"class":291},[281,506,508,510,513,515,518,520,522,525,527,529,531,534,536],{"class":283,"line":507},11,[281,509,381],{"class":314},[281,511,512],{"class":291},"\"A4\"",[281,514,442],{"class":314},[281,516,517],{"class":291},"\"B4\"",[281,519,387],{"class":314},[281,521,350],{"class":310},[281,523,524],{"class":291}," \"Total due\"",[281,526,420],{"class":314},[281,528,457],{"class":291},[281,530,461],{"class":460},[281,532,533],{"class":291},"total",[281,535,467],{"class":460},[281,537,470],{"class":291},[281,539,541,543,545,548,550],{"class":283,"line":540},12,[281,542,381],{"class":314},[281,544,517],{"class":291},[281,546,547],{"class":314},"].number_format ",[281,549,350],{"class":310},[281,551,552],{"class":291}," '\"€\"#,##0.00'\n",[281,554,556,558,560,562,564,566,568,570,572],{"class":283,"line":555},13,[281,557,381],{"class":314},[281,559,517],{"class":291},[281,561,402],{"class":314},[281,563,350],{"class":310},[281,565,407],{"class":314},[281,567,423],{"class":410},[281,569,350],{"class":310},[281,571,428],{"class":416},[281,573,431],{"class":314},[281,575,577,580,583,586,589,592,595,598,601,603,606,608,611],{"class":283,"line":576},14,[281,578,579],{"class":310},"for",[281,581,582],{"class":314}," col, label ",[281,584,585],{"class":310},"in",[281,587,588],{"class":416}," zip",[281,590,591],{"class":314},"(",[281,593,594],{"class":291},"\"ABC\"",[281,596,597],{"class":314},", [",[281,599,600],{"class":291},"\"Item\"",[281,602,420],{"class":314},[281,604,605],{"class":291},"\"Quantity\"",[281,607,420],{"class":314},[281,609,610],{"class":291},"\"Price\"",[281,612,613],{"class":314},"]):\n",[281,615,617,620,623,625,628,631,634,637,639,641],{"class":283,"line":616},15,[281,618,619],{"class":314},"    ws[",[281,621,622],{"class":310},"f",[281,624,457],{"class":291},[281,626,627],{"class":460},"{",[281,629,630],{"class":314},"col",[281,632,633],{"class":460},"}",[281,635,636],{"class":291},"6\"",[281,638,387],{"class":314},[281,640,350],{"class":310},[281,642,643],{"class":314}," label\n",[281,645,647,649,651,653,655,657,659,661,664,666,669,672,674,677,679,682],{"class":283,"line":646},16,[281,648,619],{"class":314},[281,650,622],{"class":310},[281,652,457],{"class":291},[281,654,627],{"class":460},[281,656,630],{"class":314},[281,658,633],{"class":460},[281,660,636],{"class":291},[281,662,663],{"class":314},"].fill ",[281,665,350],{"class":310},[281,667,668],{"class":314}," PatternFill(",[281,670,671],{"class":291},"\"solid\"",[281,673,420],{"class":314},[281,675,676],{"class":410},"start_color",[281,678,350],{"class":310},[281,680,681],{"class":291},"\"E7EBEF\"",[281,683,431],{"class":314},[281,685,687,689,692,694,697,699,702,704,706,709,711,714,716,718,720,722,724,727,729,731,733,735,737,740,742],{"class":283,"line":686},17,[281,688,381],{"class":314},[281,690,691],{"class":291},"\"A7\"",[281,693,442],{"class":314},[281,695,696],{"class":291},"\"B7\"",[281,698,442],{"class":314},[281,700,701],{"class":291},"\"C7\"",[281,703,387],{"class":314},[281,705,350],{"class":310},[281,707,708],{"class":291}," \"",[281,710,461],{"class":460},[281,712,713],{"class":291},"item.name",[281,715,467],{"class":460},[281,717,457],{"class":291},[281,719,420],{"class":314},[281,721,457],{"class":291},[281,723,461],{"class":460},[281,725,726],{"class":291},"item.qty",[281,728,467],{"class":460},[281,730,457],{"class":291},[281,732,420],{"class":314},[281,734,457],{"class":291},[281,736,461],{"class":460},[281,738,739],{"class":291},"item.price",[281,741,467],{"class":460},[281,743,470],{"class":291},[281,745,747,749,751,753,755],{"class":283,"line":746},18,[281,748,381],{"class":314},[281,750,701],{"class":291},[281,752,547],{"class":314},[281,754,350],{"class":310},[281,756,552],{"class":291},[281,758,760,762,765,767,769,772,774,776,778],{"class":283,"line":759},19,[281,761,381],{"class":314},[281,763,764],{"class":291},"\"A9\"",[281,766,387],{"class":314},[281,768,350],{"class":310},[281,770,771],{"class":291}," \"Thank you for your business, ",[281,773,461],{"class":460},[281,775,464],{"class":291},[281,777,467],{"class":460},[281,779,780],{"class":291},".\"\n",[281,782,784,787,790],{"class":283,"line":783},20,[281,785,786],{"class":314},"wb.save(",[281,788,789],{"class":291},"\"invoice_template.xlsx\"",[281,791,431],{"class":314},[267,793,795],{"id":794},"step-1-find-every-placeholder-in-the-workbook","Step 1 — Find every placeholder in the workbook",[10,797,798,799,802],{},"Start by listing what the template asks for. A regular expression finds ",[14,800,801],{},"{{name}}"," markers, allowing dots for grouped names, and a scan over every cell of every\nsheet returns where each one is. This inventory drives both the fill and the checks.",[272,804,806],{"className":301,"code":805,"language":303,"meta":277,"style":277},"import re\nfrom openpyxl import load_workbook\n\nPLACEHOLDER = re.compile(r\"\\{\\{\\s*([A-Za-z_][\\w.]*)\\s*\\}\\}\")\n\ndef find_placeholders(wb) -> dict[str, list[str]]:\n    found: dict[str, list[str]] = {}\n    for ws in wb.worksheets:\n        for row in ws.iter_rows():\n            for cell in row:\n                if isinstance(cell.value, str):\n                    for name in PLACEHOLDER.findall(cell.value):\n                        found.setdefault(name, []).append(f\"{ws.title}!{cell.coordinate}\")\n    return found\n\ntemplate = load_workbook(\"invoice_template.xlsx\")\nfor name, cells in find_placeholders(template).items():\n    print(f\"{name:12} {cells}\")\n",[14,807,808,815,826,830,885,889,912,931,944,957,970,986,1002,1032,1040,1044,1058,1070],{"__ignoreMap":277},[281,809,810,812],{"class":283,"line":284},[281,811,318],{"class":310},[281,813,814],{"class":314}," re\n",[281,816,817,819,821,823],{"class":283,"line":324},[281,818,311],{"class":310},[281,820,315],{"class":314},[281,822,318],{"class":310},[281,824,825],{"class":314}," load_workbook\n",[281,827,828],{"class":283,"line":337},[281,829,341],{"emptyLinePlaceholder":340},[281,831,832,835,838,841,844,846,850,853,856,859,862,865,868,871,873,876,878,881,883],{"class":283,"line":344},[281,833,834],{"class":416},"PLACEHOLDER",[281,836,837],{"class":310}," =",[281,839,840],{"class":314}," re.compile(",[281,842,843],{"class":310},"r",[281,845,457],{"class":291},[281,847,849],{"class":848},"s_b0D","\\{\\{",[281,851,852],{"class":416},"\\s",[281,854,855],{"class":310},"*",[281,857,858],{"class":416},"([",[281,860,861],{"class":460},"A-Za-z_",[281,863,864],{"class":416},"][\\w",[281,866,867],{"class":460},".",[281,869,870],{"class":416},"]",[281,872,855],{"class":310},[281,874,875],{"class":416},")\\s",[281,877,855],{"class":310},[281,879,880],{"class":848},"\\}\\}",[281,882,457],{"class":291},[281,884,431],{"class":314},[281,886,887],{"class":283,"line":356},[281,888,341],{"emptyLinePlaceholder":340},[281,890,891,894,898,901,904,907,909],{"class":283,"line":367},[281,892,893],{"class":310},"def",[281,895,897],{"class":896},"s_Opv"," find_placeholders",[281,899,900],{"class":314},"(wb) -> dict[",[281,902,903],{"class":416},"str",[281,905,906],{"class":314},", list[",[281,908,903],{"class":416},[281,910,911],{"class":314},"]]:\n",[281,913,914,917,919,921,923,926,928],{"class":283,"line":378},[281,915,916],{"class":314},"    found: dict[",[281,918,903],{"class":416},[281,920,906],{"class":314},[281,922,903],{"class":416},[281,924,925],{"class":314},"]] ",[281,927,350],{"class":310},[281,929,930],{"class":314}," {}\n",[281,932,933,936,939,941],{"class":283,"line":395},[281,934,935],{"class":310},"    for",[281,937,938],{"class":314}," ws ",[281,940,585],{"class":310},[281,942,943],{"class":314}," wb.worksheets:\n",[281,945,946,949,952,954],{"class":283,"line":434},[281,947,948],{"class":310},"        for",[281,950,951],{"class":314}," row ",[281,953,585],{"class":310},[281,955,956],{"class":314}," ws.iter_rows():\n",[281,958,959,962,965,967],{"class":283,"line":473},[281,960,961],{"class":310},"            for",[281,963,964],{"class":314}," cell ",[281,966,585],{"class":310},[281,968,969],{"class":314}," row:\n",[281,971,972,975,978,981,983],{"class":283,"line":507},[281,973,974],{"class":310},"                if",[281,976,977],{"class":416}," isinstance",[281,979,980],{"class":314},"(cell.value, ",[281,982,903],{"class":416},[281,984,985],{"class":314},"):\n",[281,987,988,991,994,996,999],{"class":283,"line":540},[281,989,990],{"class":310},"                    for",[281,992,993],{"class":314}," name ",[281,995,585],{"class":310},[281,997,998],{"class":416}," PLACEHOLDER",[281,1000,1001],{"class":314},".findall(cell.value):\n",[281,1003,1004,1007,1009,1011,1013,1016,1018,1021,1023,1026,1028,1030],{"class":283,"line":555},[281,1005,1006],{"class":314},"                        found.setdefault(name, []).append(",[281,1008,622],{"class":310},[281,1010,457],{"class":291},[281,1012,627],{"class":460},[281,1014,1015],{"class":314},"ws.title",[281,1017,633],{"class":460},[281,1019,1020],{"class":291},"!",[281,1022,627],{"class":460},[281,1024,1025],{"class":314},"cell.coordinate",[281,1027,633],{"class":460},[281,1029,457],{"class":291},[281,1031,431],{"class":314},[281,1033,1034,1037],{"class":283,"line":576},[281,1035,1036],{"class":310},"    return",[281,1038,1039],{"class":314}," found\n",[281,1041,1042],{"class":283,"line":616},[281,1043,341],{"emptyLinePlaceholder":340},[281,1045,1046,1049,1051,1054,1056],{"class":283,"line":646},[281,1047,1048],{"class":314},"template ",[281,1050,350],{"class":310},[281,1052,1053],{"class":314}," load_workbook(",[281,1055,789],{"class":291},[281,1057,431],{"class":314},[281,1059,1060,1062,1065,1067],{"class":283,"line":686},[281,1061,579],{"class":310},[281,1063,1064],{"class":314}," name, cells ",[281,1066,585],{"class":310},[281,1068,1069],{"class":314}," find_placeholders(template).items():\n",[281,1071,1072,1075,1077,1079,1081,1083,1086,1089,1091,1094,1097,1099,1101],{"class":283,"line":746},[281,1073,1074],{"class":416},"    print",[281,1076,591],{"class":314},[281,1078,622],{"class":310},[281,1080,457],{"class":291},[281,1082,627],{"class":460},[281,1084,1085],{"class":314},"name",[281,1087,1088],{"class":310},":12",[281,1090,633],{"class":460},[281,1092,1093],{"class":460}," {",[281,1095,1096],{"class":314},"cells",[281,1098,633],{"class":460},[281,1100,457],{"class":291},[281,1102,431],{"class":314},[10,1104,1105,1106,1109,1110,1113,1114,1116],{},"The ",[14,1107,1108],{},"\\s*"," inside the braces tolerates ",[14,1111,1112],{},"{{ customer }}"," with spaces, which people type as often as not. Allowing only identifier characters keeps a stray ",[14,1115,461],{}," in an\nordinary sentence from being mistaken for a marker.",[267,1118,1120],{"id":1119},"step-2-replace-whole-cell-placeholders-with-typed-values","Step 2 — Replace whole-cell placeholders with typed values",[10,1122,1123,1124,1128,1129,1132],{},"When a cell contains ",[1125,1126,1127],"em",{},"only"," a placeholder, the replacement should become the cell's value with its real type: a number stays a number so the template's currency format\napplies and formulas can use it, and a date stays a date. Replacing it with a string would display ",[14,1130,1131],{},"1234.5"," left-aligned as text.",[272,1134,1136],{"className":301,"code":1135,"language":303,"meta":277,"style":277},"def fill_scalars(wb, values: dict) -> list[str]:\n    missing = []\n    for ws in wb.worksheets:\n        for row in ws.iter_rows():\n            for cell in row:\n                if not isinstance(cell.value, str):\n                    continue\n                whole = PLACEHOLDER.fullmatch(cell.value.strip())\n                if whole and whole.group(1) in values:\n                    cell.value = values[whole.group(1)]             # keeps the value's type\n                    continue\n\n                def substitute(match):\n                    name = match.group(1)\n                    if name in values:\n                        return str(values[name])\n                    missing.append(f\"{ws.title}!{cell.coordinate}:{name}\")\n                    return match.group(0)\n\n                if PLACEHOLDER.search(cell.value) and not cell.value.strip().startswith(\"{{item.\"):\n                    cell.value = PLACEHOLDER.sub(substitute, cell.value)\n    return missing\n\nwb = load_workbook(\"invoice_template.xlsx\")\nmissing = fill_scalars(wb, {\"customer\": \"Northwind Traders\", \"period\": \"August 2026\", \"total\": 4630.5})\nprint(wb[\"Invoice\"][\"B2\"].value, \"|\", wb[\"Invoice\"][\"B3\"].value, \"|\", wb[\"Invoice\"][\"B4\"].value, wb[\"Invoice\"][\"B4\"].number_format)\nprint(\"missing:\", missing)\n",[14,1137,1138,1159,1169,1179,1189,1199,1214,1219,1231,1254,1273,1277,1281,1292,1306,1317,1328,1364,1375,1379,1404,1416,1424,1429,1442,1485,1541],{"__ignoreMap":277},[281,1139,1140,1142,1145,1148,1151,1154,1156],{"class":283,"line":284},[281,1141,893],{"class":310},[281,1143,1144],{"class":896}," fill_scalars",[281,1146,1147],{"class":314},"(wb, values: ",[281,1149,1150],{"class":416},"dict",[281,1152,1153],{"class":314},") -> list[",[281,1155,903],{"class":416},[281,1157,1158],{"class":314},"]:\n",[281,1160,1161,1164,1166],{"class":283,"line":324},[281,1162,1163],{"class":314},"    missing ",[281,1165,350],{"class":310},[281,1167,1168],{"class":314}," []\n",[281,1170,1171,1173,1175,1177],{"class":283,"line":337},[281,1172,935],{"class":310},[281,1174,938],{"class":314},[281,1176,585],{"class":310},[281,1178,943],{"class":314},[281,1180,1181,1183,1185,1187],{"class":283,"line":344},[281,1182,948],{"class":310},[281,1184,951],{"class":314},[281,1186,585],{"class":310},[281,1188,956],{"class":314},[281,1190,1191,1193,1195,1197],{"class":283,"line":356},[281,1192,961],{"class":310},[281,1194,964],{"class":314},[281,1196,585],{"class":310},[281,1198,969],{"class":314},[281,1200,1201,1203,1206,1208,1210,1212],{"class":283,"line":367},[281,1202,974],{"class":310},[281,1204,1205],{"class":310}," not",[281,1207,977],{"class":416},[281,1209,980],{"class":314},[281,1211,903],{"class":416},[281,1213,985],{"class":314},[281,1215,1216],{"class":283,"line":378},[281,1217,1218],{"class":310},"                    continue\n",[281,1220,1221,1224,1226,1228],{"class":283,"line":395},[281,1222,1223],{"class":314},"                whole ",[281,1225,350],{"class":310},[281,1227,998],{"class":416},[281,1229,1230],{"class":314},".fullmatch(cell.value.strip())\n",[281,1232,1233,1235,1238,1241,1244,1246,1249,1251],{"class":283,"line":434},[281,1234,974],{"class":310},[281,1236,1237],{"class":314}," whole ",[281,1239,1240],{"class":310},"and",[281,1242,1243],{"class":314}," whole.group(",[281,1245,99],{"class":416},[281,1247,1248],{"class":314},") ",[281,1250,585],{"class":310},[281,1252,1253],{"class":314}," values:\n",[281,1255,1256,1259,1261,1264,1266,1269],{"class":283,"line":473},[281,1257,1258],{"class":314},"                    cell.value ",[281,1260,350],{"class":310},[281,1262,1263],{"class":314}," values[whole.group(",[281,1265,99],{"class":416},[281,1267,1268],{"class":314},")]             ",[281,1270,1272],{"class":1271},"s-wDw","# keeps the value's type\n",[281,1274,1275],{"class":283,"line":507},[281,1276,1218],{"class":310},[281,1278,1279],{"class":283,"line":540},[281,1280,341],{"emptyLinePlaceholder":340},[281,1282,1283,1286,1289],{"class":283,"line":555},[281,1284,1285],{"class":310},"                def",[281,1287,1288],{"class":896}," substitute",[281,1290,1291],{"class":314},"(match):\n",[281,1293,1294,1297,1299,1302,1304],{"class":283,"line":576},[281,1295,1296],{"class":314},"                    name ",[281,1298,350],{"class":310},[281,1300,1301],{"class":314}," match.group(",[281,1303,99],{"class":416},[281,1305,431],{"class":314},[281,1307,1308,1311,1313,1315],{"class":283,"line":616},[281,1309,1310],{"class":310},"                    if",[281,1312,993],{"class":314},[281,1314,585],{"class":310},[281,1316,1253],{"class":314},[281,1318,1319,1322,1325],{"class":283,"line":646},[281,1320,1321],{"class":310},"                        return",[281,1323,1324],{"class":416}," str",[281,1326,1327],{"class":314},"(values[name])\n",[281,1329,1330,1333,1335,1337,1339,1341,1343,1345,1347,1349,1351,1354,1356,1358,1360,1362],{"class":283,"line":686},[281,1331,1332],{"class":314},"                    missing.append(",[281,1334,622],{"class":310},[281,1336,457],{"class":291},[281,1338,627],{"class":460},[281,1340,1015],{"class":314},[281,1342,633],{"class":460},[281,1344,1020],{"class":291},[281,1346,627],{"class":460},[281,1348,1025],{"class":314},[281,1350,633],{"class":460},[281,1352,1353],{"class":291},":",[281,1355,627],{"class":460},[281,1357,1085],{"class":314},[281,1359,633],{"class":460},[281,1361,457],{"class":291},[281,1363,431],{"class":314},[281,1365,1366,1369,1371,1373],{"class":283,"line":746},[281,1367,1368],{"class":310},"                    return",[281,1370,1301],{"class":314},[281,1372,54],{"class":416},[281,1374,431],{"class":314},[281,1376,1377],{"class":283,"line":759},[281,1378,341],{"emptyLinePlaceholder":340},[281,1380,1381,1383,1385,1388,1390,1392,1395,1397,1399,1402],{"class":283,"line":783},[281,1382,974],{"class":310},[281,1384,998],{"class":416},[281,1386,1387],{"class":314},".search(cell.value) ",[281,1389,1240],{"class":310},[281,1391,1205],{"class":310},[281,1393,1394],{"class":314}," cell.value.strip().startswith(",[281,1396,457],{"class":291},[281,1398,461],{"class":460},[281,1400,1401],{"class":291},"item.\"",[281,1403,985],{"class":314},[281,1405,1407,1409,1411,1413],{"class":283,"line":1406},21,[281,1408,1258],{"class":314},[281,1410,350],{"class":310},[281,1412,998],{"class":416},[281,1414,1415],{"class":314},".sub(substitute, cell.value)\n",[281,1417,1419,1421],{"class":283,"line":1418},22,[281,1420,1036],{"class":310},[281,1422,1423],{"class":314}," missing\n",[281,1425,1427],{"class":283,"line":1426},23,[281,1428,341],{"emptyLinePlaceholder":340},[281,1430,1432,1434,1436,1438,1440],{"class":283,"line":1431},24,[281,1433,347],{"class":314},[281,1435,350],{"class":310},[281,1437,1053],{"class":314},[281,1439,789],{"class":291},[281,1441,431],{"class":314},[281,1443,1445,1448,1450,1453,1456,1459,1462,1464,1467,1469,1472,1474,1477,1479,1482],{"class":283,"line":1444},25,[281,1446,1447],{"class":314},"missing ",[281,1449,350],{"class":310},[281,1451,1452],{"class":314}," fill_scalars(wb, {",[281,1454,1455],{"class":291},"\"customer\"",[281,1457,1458],{"class":314},": ",[281,1460,1461],{"class":291},"\"Northwind Traders\"",[281,1463,420],{"class":314},[281,1465,1466],{"class":291},"\"period\"",[281,1468,1458],{"class":314},[281,1470,1471],{"class":291},"\"August 2026\"",[281,1473,420],{"class":314},[281,1475,1476],{"class":291},"\"total\"",[281,1478,1458],{"class":314},[281,1480,1481],{"class":416},"4630.5",[281,1483,1484],{"class":314},"})\n",[281,1486,1488,1491,1494,1497,1500,1502,1505,1508,1511,1513,1515,1517,1519,1521,1523,1525,1527,1529,1532,1534,1536,1538],{"class":283,"line":1487},26,[281,1489,1490],{"class":416},"print",[281,1492,1493],{"class":314},"(wb[",[281,1495,1496],{"class":291},"\"Invoice\"",[281,1498,1499],{"class":314},"][",[281,1501,445],{"class":291},[281,1503,1504],{"class":314},"].value, ",[281,1506,1507],{"class":291},"\"|\"",[281,1509,1510],{"class":314},", wb[",[281,1512,1496],{"class":291},[281,1514,1499],{"class":314},[281,1516,483],{"class":291},[281,1518,1504],{"class":314},[281,1520,1507],{"class":291},[281,1522,1510],{"class":314},[281,1524,1496],{"class":291},[281,1526,1499],{"class":314},[281,1528,517],{"class":291},[281,1530,1531],{"class":314},"].value, wb[",[281,1533,1496],{"class":291},[281,1535,1499],{"class":314},[281,1537,517],{"class":291},[281,1539,1540],{"class":314},"].number_format)\n",[281,1542,1544,1546,1548,1551],{"class":283,"line":1543},27,[281,1545,1490],{"class":416},[281,1547,591],{"class":314},[281,1549,1550],{"class":291},"\"missing:\"",[281,1552,1553],{"class":314},", missing)\n",[10,1555,1556,1557,1559,1560,1563],{},"Placeholders embedded in text — ",[14,1558,161],{}," — are substituted as strings, because the cell is text anyway. Assigning ",[14,1561,1562],{},"cell.value"," leaves the cell's font, fill,\nborder and number format untouched, which is why the bold, currency-formatted total survives. Line-item markers are skipped here and handled in the next step.",[33,1565,42,1571,42,1574,42,1577,42,1581,42,1587,42,1593,42,1596,42,1600,42,1604,42,1608,42,1611,42,1616,42,1620,42,1623,42,1626,42,1629],{"viewBox":1566,"role":36,"ariaLabelledBy":1567,"xmlns":40,"style":1570},"0 0 760 191",[1568,1569],"ph-types-t","ph-types-d","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[44,1572,1573],{"id":1568},"Whole-cell and inline placeholders are replaced differently",[48,1575,1576],{"id":1569},"A cell holding only {{total}} receives the number 4630.5, so its currency format applies. A cell holding Services for {{period}} receives text, because the placeholder is part of a sentence.",[52,1578],{"x":54,"y":54,"width":1579,"height":1580,"fill":57},"760","191",[52,1582],{"x":60,"y":60,"width":1583,"height":1584,"rx":1585,"fill":127,"stroke":128,"style":1586},"350","127","14","stroke-width:2px",[71,1588,1592],{"x":1589,"y":1590,"style":1591},"195","46","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","whole cell: {{total}}",[283,1594],{"x1":61,"y1":68,"x2":1595,"y2":68,"stroke":128,"style":65},"354",[71,1597,1599],{"x":1589,"y":1598,"style":123},"78","value becomes 4630.5",[71,1601,1603],{"x":1589,"y":1602,"style":123},"101","type kept: number",[71,1605,1607],{"x":1589,"y":1606,"style":123},"124","shows €4,630.50",[52,1609],{"x":1610,"y":60,"width":1583,"height":1584,"rx":1585,"fill":102,"stroke":103,"style":1586},"390",[71,1612,1615],{"x":1613,"y":1590,"style":1614},"565","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","inline: for {{period}}",[283,1617],{"x1":1618,"y1":68,"x2":1619,"y2":68,"stroke":103,"style":65},"406","724",[71,1621,1622],{"x":1613,"y":1598,"style":123},"text substitution",[71,1624,1625],{"x":1613,"y":1602,"style":123},"result is a string",[71,1627,1628],{"x":1613,"y":1606,"style":123},"\"Services for August 2026\"",[71,1630,1633],{"x":1631,"y":1632,"style":264},"380","177","never turn a number into text just to fill a template",[267,1635,1637],{"id":1636},"step-3-expand-a-repeating-row-for-line-items","Step 3 — Expand a repeating row for line items",[10,1639,1640,1641,1644],{},"Invoices and statements have a variable number of rows. Mark one template row with ",[14,1642,1643],{},"{{item.*}}"," placeholders, then insert as many copies as there are items, copying the\ntemplate row's styles to each, and fill the values. Everything below the table moves down to make room.",[272,1646,1648],{"className":301,"code":1647,"language":303,"meta":277,"style":277},"from copy import copy\n\ndef expand_rows(ws, items: list[dict], prefix: str = \"item\") -> int:\n    marker_row = next(\n        cell.row for row in ws.iter_rows() for cell in row\n        if isinstance(cell.value, str) and f\"{{{{{prefix}.\" in cell.value\n    )\n    template_cells = [\n        (cell.column, cell.value, copy(cell._style)) for cell in ws[marker_row]\n    ]\n    if len(items) > 1:\n        ws.insert_rows(marker_row + 1, amount=len(items) - 1)\n\n    for offset, item in enumerate(items):\n        target_row = marker_row + offset\n        for column, raw, style in template_cells:\n            cell = ws.cell(row=target_row, column=column)\n            cell._style = copy(style)\n            match = PLACEHOLDER.fullmatch(raw.strip()) if isinstance(raw, str) else None\n            if match and match.group(1).startswith(prefix + \".\"):\n                cell.value = item[match.group(1).split(\".\", 1)[1]]\n            else:\n                cell.value = raw\n    return marker_row\n\nitems = [\n    {\"name\": \"Consulting\", \"qty\": 12, \"price\": 250.0},\n    {\"name\": \"Training day\", \"qty\": 1, \"price\": 1200.0},\n    {\"name\": \"Travel\", \"qty\": 1, \"price\": 430.5},\n]\nfirst = expand_rows(wb[\"Invoice\"], items)\nprint(\"items start on row\", first, \"; closing line now on row\", first + len(items) + 1)\n",[14,1649,1650,1662,1666,1697,1710,1733,1770,1775,1785,1799,1804,1823,1852,1856,1871,1886,1898,1924,1934,1964,1988,2018,2025,2034,2041,2045,2054,2090,2121,2152,2158,2174],{"__ignoreMap":277},[281,1651,1652,1654,1657,1659],{"class":283,"line":284},[281,1653,311],{"class":310},[281,1655,1656],{"class":314}," copy ",[281,1658,318],{"class":310},[281,1660,1661],{"class":314}," copy\n",[281,1663,1664],{"class":283,"line":324},[281,1665,341],{"emptyLinePlaceholder":340},[281,1667,1668,1670,1673,1676,1678,1681,1683,1685,1688,1691,1694],{"class":283,"line":337},[281,1669,893],{"class":310},[281,1671,1672],{"class":896}," expand_rows",[281,1674,1675],{"class":314},"(ws, items: list[",[281,1677,1150],{"class":416},[281,1679,1680],{"class":314},"], prefix: ",[281,1682,903],{"class":416},[281,1684,837],{"class":310},[281,1686,1687],{"class":291}," \"item\"",[281,1689,1690],{"class":314},") -> ",[281,1692,1693],{"class":416},"int",[281,1695,1696],{"class":314},":\n",[281,1698,1699,1702,1704,1707],{"class":283,"line":344},[281,1700,1701],{"class":314},"    marker_row ",[281,1703,350],{"class":310},[281,1705,1706],{"class":416}," next",[281,1708,1709],{"class":314},"(\n",[281,1711,1712,1715,1717,1719,1721,1724,1726,1728,1730],{"class":283,"line":356},[281,1713,1714],{"class":314},"        cell.row ",[281,1716,579],{"class":310},[281,1718,951],{"class":314},[281,1720,585],{"class":310},[281,1722,1723],{"class":314}," ws.iter_rows() ",[281,1725,579],{"class":310},[281,1727,964],{"class":314},[281,1729,585],{"class":310},[281,1731,1732],{"class":314}," row\n",[281,1734,1735,1738,1740,1742,1744,1746,1748,1751,1753,1756,1759,1761,1764,1767],{"class":283,"line":367},[281,1736,1737],{"class":310},"        if",[281,1739,977],{"class":416},[281,1741,980],{"class":314},[281,1743,903],{"class":416},[281,1745,1248],{"class":314},[281,1747,1240],{"class":310},[281,1749,1750],{"class":310}," f",[281,1752,457],{"class":291},[281,1754,1755],{"class":460},"{{{{{",[281,1757,1758],{"class":314},"prefix",[281,1760,633],{"class":460},[281,1762,1763],{"class":291},".\"",[281,1765,1766],{"class":310}," in",[281,1768,1769],{"class":314}," cell.value\n",[281,1771,1772],{"class":283,"line":378},[281,1773,1774],{"class":314},"    )\n",[281,1776,1777,1780,1782],{"class":283,"line":395},[281,1778,1779],{"class":314},"    template_cells ",[281,1781,350],{"class":310},[281,1783,1784],{"class":314}," [\n",[281,1786,1787,1790,1792,1794,1796],{"class":283,"line":434},[281,1788,1789],{"class":314},"        (cell.column, cell.value, copy(cell._style)) ",[281,1791,579],{"class":310},[281,1793,964],{"class":314},[281,1795,585],{"class":310},[281,1797,1798],{"class":314}," ws[marker_row]\n",[281,1800,1801],{"class":283,"line":473},[281,1802,1803],{"class":314},"    ]\n",[281,1805,1806,1809,1812,1815,1818,1821],{"class":283,"line":507},[281,1807,1808],{"class":310},"    if",[281,1810,1811],{"class":416}," len",[281,1813,1814],{"class":314},"(items) ",[281,1816,1817],{"class":310},">",[281,1819,1820],{"class":416}," 1",[281,1822,1696],{"class":314},[281,1824,1825,1828,1831,1833,1835,1838,1840,1843,1845,1848,1850],{"class":283,"line":540},[281,1826,1827],{"class":314},"        ws.insert_rows(marker_row ",[281,1829,1830],{"class":310},"+",[281,1832,1820],{"class":416},[281,1834,420],{"class":314},[281,1836,1837],{"class":410},"amount",[281,1839,350],{"class":310},[281,1841,1842],{"class":416},"len",[281,1844,1814],{"class":314},[281,1846,1847],{"class":310},"-",[281,1849,1820],{"class":416},[281,1851,431],{"class":314},[281,1853,1854],{"class":283,"line":555},[281,1855,341],{"emptyLinePlaceholder":340},[281,1857,1858,1860,1863,1865,1868],{"class":283,"line":576},[281,1859,935],{"class":310},[281,1861,1862],{"class":314}," offset, item ",[281,1864,585],{"class":310},[281,1866,1867],{"class":416}," enumerate",[281,1869,1870],{"class":314},"(items):\n",[281,1872,1873,1876,1878,1881,1883],{"class":283,"line":616},[281,1874,1875],{"class":314},"        target_row ",[281,1877,350],{"class":310},[281,1879,1880],{"class":314}," marker_row ",[281,1882,1830],{"class":310},[281,1884,1885],{"class":314}," offset\n",[281,1887,1888,1890,1893,1895],{"class":283,"line":646},[281,1889,948],{"class":310},[281,1891,1892],{"class":314}," column, raw, style ",[281,1894,585],{"class":310},[281,1896,1897],{"class":314}," template_cells:\n",[281,1899,1900,1903,1905,1908,1911,1913,1916,1919,1921],{"class":283,"line":686},[281,1901,1902],{"class":314},"            cell ",[281,1904,350],{"class":310},[281,1906,1907],{"class":314}," ws.cell(",[281,1909,1910],{"class":410},"row",[281,1912,350],{"class":310},[281,1914,1915],{"class":314},"target_row, ",[281,1917,1918],{"class":410},"column",[281,1920,350],{"class":310},[281,1922,1923],{"class":314},"column)\n",[281,1925,1926,1929,1931],{"class":283,"line":746},[281,1927,1928],{"class":314},"            cell._style ",[281,1930,350],{"class":310},[281,1932,1933],{"class":314}," copy(style)\n",[281,1935,1936,1939,1941,1943,1946,1949,1951,1954,1956,1958,1961],{"class":283,"line":759},[281,1937,1938],{"class":314},"            match ",[281,1940,350],{"class":310},[281,1942,998],{"class":416},[281,1944,1945],{"class":314},".fullmatch(raw.strip()) ",[281,1947,1948],{"class":310},"if",[281,1950,977],{"class":416},[281,1952,1953],{"class":314},"(raw, ",[281,1955,903],{"class":416},[281,1957,1248],{"class":314},[281,1959,1960],{"class":310},"else",[281,1962,1963],{"class":416}," None\n",[281,1965,1966,1969,1972,1974,1976,1978,1981,1983,1986],{"class":283,"line":783},[281,1967,1968],{"class":310},"            if",[281,1970,1971],{"class":314}," match ",[281,1973,1240],{"class":310},[281,1975,1301],{"class":314},[281,1977,99],{"class":416},[281,1979,1980],{"class":314},").startswith(prefix ",[281,1982,1830],{"class":310},[281,1984,1985],{"class":291}," \".\"",[281,1987,985],{"class":314},[281,1989,1990,1993,1995,1998,2000,2003,2006,2008,2010,2013,2015],{"class":283,"line":1406},[281,1991,1992],{"class":314},"                cell.value ",[281,1994,350],{"class":310},[281,1996,1997],{"class":314}," item[match.group(",[281,1999,99],{"class":416},[281,2001,2002],{"class":314},").split(",[281,2004,2005],{"class":291},"\".\"",[281,2007,420],{"class":314},[281,2009,99],{"class":416},[281,2011,2012],{"class":314},")[",[281,2014,99],{"class":416},[281,2016,2017],{"class":314},"]]\n",[281,2019,2020,2023],{"class":283,"line":1418},[281,2021,2022],{"class":310},"            else",[281,2024,1696],{"class":314},[281,2026,2027,2029,2031],{"class":283,"line":1426},[281,2028,1992],{"class":314},[281,2030,350],{"class":310},[281,2032,2033],{"class":314}," raw\n",[281,2035,2036,2038],{"class":283,"line":1431},[281,2037,1036],{"class":310},[281,2039,2040],{"class":314}," marker_row\n",[281,2042,2043],{"class":283,"line":1444},[281,2044,341],{"emptyLinePlaceholder":340},[281,2046,2047,2050,2052],{"class":283,"line":1487},[281,2048,2049],{"class":314},"items ",[281,2051,350],{"class":310},[281,2053,1784],{"class":314},[281,2055,2056,2059,2062,2064,2067,2069,2072,2074,2077,2079,2082,2084,2087],{"class":283,"line":1543},[281,2057,2058],{"class":314},"    {",[281,2060,2061],{"class":291},"\"name\"",[281,2063,1458],{"class":314},[281,2065,2066],{"class":291},"\"Consulting\"",[281,2068,420],{"class":314},[281,2070,2071],{"class":291},"\"qty\"",[281,2073,1458],{"class":314},[281,2075,2076],{"class":416},"12",[281,2078,420],{"class":314},[281,2080,2081],{"class":291},"\"price\"",[281,2083,1458],{"class":314},[281,2085,2086],{"class":416},"250.0",[281,2088,2089],{"class":314},"},\n",[281,2091,2093,2095,2097,2099,2102,2104,2106,2108,2110,2112,2114,2116,2119],{"class":283,"line":2092},28,[281,2094,2058],{"class":314},[281,2096,2061],{"class":291},[281,2098,1458],{"class":314},[281,2100,2101],{"class":291},"\"Training day\"",[281,2103,420],{"class":314},[281,2105,2071],{"class":291},[281,2107,1458],{"class":314},[281,2109,99],{"class":416},[281,2111,420],{"class":314},[281,2113,2081],{"class":291},[281,2115,1458],{"class":314},[281,2117,2118],{"class":416},"1200.0",[281,2120,2089],{"class":314},[281,2122,2124,2126,2128,2130,2133,2135,2137,2139,2141,2143,2145,2147,2150],{"class":283,"line":2123},29,[281,2125,2058],{"class":314},[281,2127,2061],{"class":291},[281,2129,1458],{"class":314},[281,2131,2132],{"class":291},"\"Travel\"",[281,2134,420],{"class":314},[281,2136,2071],{"class":291},[281,2138,1458],{"class":314},[281,2140,99],{"class":416},[281,2142,420],{"class":314},[281,2144,2081],{"class":291},[281,2146,1458],{"class":314},[281,2148,2149],{"class":416},"430.5",[281,2151,2089],{"class":314},[281,2153,2155],{"class":283,"line":2154},30,[281,2156,2157],{"class":314},"]\n",[281,2159,2161,2164,2166,2169,2171],{"class":283,"line":2160},31,[281,2162,2163],{"class":314},"first ",[281,2165,350],{"class":310},[281,2167,2168],{"class":314}," expand_rows(wb[",[281,2170,1496],{"class":291},[281,2172,2173],{"class":314},"], items)\n",[281,2175,2177,2179,2181,2184,2187,2190,2193,2195,2197,2199,2201,2203],{"class":283,"line":2176},32,[281,2178,1490],{"class":416},[281,2180,591],{"class":314},[281,2182,2183],{"class":291},"\"items start on row\"",[281,2185,2186],{"class":314},", first, ",[281,2188,2189],{"class":291},"\"; closing line now on row\"",[281,2191,2192],{"class":314},", first ",[281,2194,1830],{"class":310},[281,2196,1811],{"class":416},[281,2198,1814],{"class":314},[281,2200,1830],{"class":310},[281,2202,1820],{"class":416},[281,2204,431],{"class":314},[10,2206,2207,2210,2211,2214,2215,2217,2218,867],{},[14,2208,2209],{},"insert_rows"," shifts cell values and styles below the insertion point, but it does not rewrite formulas that refer to shifted cells, and it does not move merged ranges or\nconditional formatting. Keep formulas that total the table ",[1125,2212,2213],{},"above"," it, or write the total as a value as this template does with ",[14,2216,190],{},". The limits of row insertion are\ndescribed in ",[27,2219,2221],{"href":2220},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Finsert-and-delete-rows-and-columns-with-openpyxl\u002F","Insert and Delete Rows and Columns with openpyxl",[267,2223,2225],{"id":2224},"step-4-fill-headers-and-footers-too","Step 4 — Fill headers and footers too",[10,2227,2228],{},"Page headers and footers are where report names and periods often belong, and they are invisible to a cell scan. Apply the same substitution to their text.",[272,2230,2232],{"className":301,"code":2231,"language":303,"meta":277,"style":277},"def fill_header_footer(wb, values: dict) -> None:\n    for ws in wb.worksheets:\n        for part in (ws.oddHeader, ws.oddFooter, ws.evenHeader, ws.evenFooter):\n            for section in (part.left, part.center, part.right):\n                if section.text:\n                    section.text = PLACEHOLDER.sub(\n                        lambda m: str(values.get(m.group(1), m.group(0))), section.text)\n\nwb[\"Invoice\"].oddFooter.center.text = \"Northwind invoice — {{period}}\"\nfill_header_footer(wb, {\"period\": \"August 2026\"})\nprint(wb[\"Invoice\"].oddFooter.center.text)\n",[14,2233,2234,2252,2262,2274,2286,2293,2305,2328,2332,2355,2368],{"__ignoreMap":277},[281,2235,2236,2238,2241,2243,2245,2247,2250],{"class":283,"line":284},[281,2237,893],{"class":310},[281,2239,2240],{"class":896}," fill_header_footer",[281,2242,1147],{"class":314},[281,2244,1150],{"class":416},[281,2246,1690],{"class":314},[281,2248,2249],{"class":416},"None",[281,2251,1696],{"class":314},[281,2253,2254,2256,2258,2260],{"class":283,"line":324},[281,2255,935],{"class":310},[281,2257,938],{"class":314},[281,2259,585],{"class":310},[281,2261,943],{"class":314},[281,2263,2264,2266,2269,2271],{"class":283,"line":337},[281,2265,948],{"class":310},[281,2267,2268],{"class":314}," part ",[281,2270,585],{"class":310},[281,2272,2273],{"class":314}," (ws.oddHeader, ws.oddFooter, ws.evenHeader, ws.evenFooter):\n",[281,2275,2276,2278,2281,2283],{"class":283,"line":344},[281,2277,961],{"class":310},[281,2279,2280],{"class":314}," section ",[281,2282,585],{"class":310},[281,2284,2285],{"class":314}," (part.left, part.center, part.right):\n",[281,2287,2288,2290],{"class":283,"line":356},[281,2289,974],{"class":310},[281,2291,2292],{"class":314}," section.text:\n",[281,2294,2295,2298,2300,2302],{"class":283,"line":367},[281,2296,2297],{"class":314},"                    section.text ",[281,2299,350],{"class":310},[281,2301,998],{"class":416},[281,2303,2304],{"class":314},".sub(\n",[281,2306,2307,2310,2313,2315,2318,2320,2323,2325],{"class":283,"line":378},[281,2308,2309],{"class":310},"                        lambda",[281,2311,2312],{"class":314}," m: ",[281,2314,903],{"class":416},[281,2316,2317],{"class":314},"(values.get(m.group(",[281,2319,99],{"class":416},[281,2321,2322],{"class":314},"), m.group(",[281,2324,54],{"class":416},[281,2326,2327],{"class":314},"))), section.text)\n",[281,2329,2330],{"class":283,"line":395},[281,2331,341],{"emptyLinePlaceholder":340},[281,2333,2334,2337,2339,2342,2344,2347,2349,2351,2353],{"class":283,"line":434},[281,2335,2336],{"class":314},"wb[",[281,2338,1496],{"class":291},[281,2340,2341],{"class":314},"].oddFooter.center.text ",[281,2343,350],{"class":310},[281,2345,2346],{"class":291}," \"Northwind invoice — ",[281,2348,461],{"class":460},[281,2350,500],{"class":291},[281,2352,467],{"class":460},[281,2354,470],{"class":291},[281,2356,2357,2360,2362,2364,2366],{"class":283,"line":473},[281,2358,2359],{"class":314},"fill_header_footer(wb, {",[281,2361,1466],{"class":291},[281,2363,1458],{"class":314},[281,2365,1471],{"class":291},[281,2367,1484],{"class":314},[281,2369,2370,2372,2374,2376],{"class":283,"line":507},[281,2371,1490],{"class":416},[281,2373,1493],{"class":314},[281,2375,1496],{"class":291},[281,2377,2378],{"class":314},"].oddFooter.center.text)\n",[10,2380,2381],{},"Unknown names are left in place rather than replaced with an empty string, so the check in the next step can report them.",[33,2383,42,2388,42,2391,42,2394,42,2397,42,2400,42,2405,42,2409,42,2414,42,2419,42,2422,42,2427,42,2430,42,2434,42,2437,42,2440,42,2445,42,2448,42,2452,42,2455,42,2458,42,2463,42,2466],{"viewBox":2384,"role":36,"ariaLabelledBy":2385,"xmlns":40,"style":1570},"0 0 760 136",[2386,2387],"ph-fill-t","ph-fill-d",[44,2389,2390],{"id":2386},"The order of a placeholder fill",[48,2392,2393],{"id":2387},"Scan the template for placeholders, fill scalar placeholders in cells, expand repeating rows for line items, fill headers and footers, then scan again and fail if any placeholder remains.",[52,2395],{"x":54,"y":54,"width":1579,"height":2396,"fill":57},"136",[52,2398],{"x":60,"y":60,"width":138,"height":2399,"rx":2076,"fill":63,"stroke":64,"style":1586},"66",[71,2401,2404],{"x":2402,"y":94,"style":2403},"95","font-size:14px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","inventory",[71,2406,2408],{"x":2402,"y":2407,"style":216},"72","find {{names}}",[283,2410],{"x1":2411,"y1":2412,"x2":2413,"y2":2412,"stroke":103,"style":1586},"175","53","204",[2415,2416],"polygon",{"points":2417,"fill":2418},"207,53 198,48 198,58","#5b5cf0",[52,2420],{"x":2421,"y":60,"width":138,"height":2399,"rx":2076,"fill":102,"stroke":103,"style":1586},"210",[71,2423,2426],{"x":2424,"y":94,"style":2425},"285","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","fill values",[71,2428,2429],{"x":2424,"y":2407,"style":123},"typed or inline",[283,2431],{"x1":2432,"y1":2412,"x2":2433,"y2":2412,"stroke":103,"style":1586},"365","394",[2415,2435],{"points":2436,"fill":2418},"397,53 388,48 388,58",[52,2438],{"x":2439,"y":60,"width":138,"height":2399,"rx":2076,"fill":237,"stroke":238,"style":1586},"400",[71,2441,2444],{"x":2442,"y":94,"style":2443},"475","font-size:14px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","expand rows",[71,2446,2447],{"x":2442,"y":2407,"style":123},"line items",[283,2449],{"x1":2450,"y1":2412,"x2":2451,"y2":2412,"stroke":103,"style":1586},"555","584",[2415,2453],{"points":2454,"fill":2418},"587,53 578,48 578,58",[52,2456],{"x":2457,"y":60,"width":138,"height":2399,"rx":2076,"fill":127,"stroke":128,"style":1586},"590",[71,2459,2462],{"x":2460,"y":94,"style":2461},"665","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","verify",[71,2464,2465],{"x":2460,"y":2407,"style":123},"none left, save",[71,2467,2469],{"x":1631,"y":2468,"style":264},"120","a report with a visible {{placeholder}} must never be sent",[267,2471,2473],{"id":2472},"step-5-refuse-to-save-a-half-filled-report","Step 5 — Refuse to save a half-filled report",[10,2475,2476,2477,2480],{},"A report that reaches a customer with ",[14,2478,2479],{},"{{po_number}}"," printed on it is worse than a report that is late. After filling, scan again; if any placeholder remains, raise with the\nlist of names and cells instead of saving.",[272,2482,2484],{"className":301,"code":2483,"language":303,"meta":277,"style":277},"def assert_filled(wb) -> None:\n    leftover = find_placeholders(wb)\n    if leftover:\n        detail = \"; \".join(f\"{name} at {', '.join(cells)}\" for name, cells in leftover.items())\n        raise ValueError(f\"unfilled placeholders: {detail}\")\n\nassert_filled(wb)                           # raises with names and cells if anything is left\nwb.save(\"invoice_northwind.xlsx\")\n\nsaved = load_workbook(\"invoice_northwind.xlsx\")[\"Invoice\"]\nprint([saved.cell(row, 1).value for row in range(7, 10)])   # the three line items\nprint(saved[\"A11\"].value)                   # closing line, moved down by the inserted rows\n",[14,2485,2486,2500,2510,2517,2565,2591,2595,2603,2612,2616,2633,2669],{"__ignoreMap":277},[281,2487,2488,2490,2493,2496,2498],{"class":283,"line":284},[281,2489,893],{"class":310},[281,2491,2492],{"class":896}," assert_filled",[281,2494,2495],{"class":314},"(wb) -> ",[281,2497,2249],{"class":416},[281,2499,1696],{"class":314},[281,2501,2502,2505,2507],{"class":283,"line":324},[281,2503,2504],{"class":314},"    leftover ",[281,2506,350],{"class":310},[281,2508,2509],{"class":314}," find_placeholders(wb)\n",[281,2511,2512,2514],{"class":283,"line":337},[281,2513,1808],{"class":310},[281,2515,2516],{"class":314}," leftover:\n",[281,2518,2519,2522,2524,2527,2530,2532,2534,2536,2538,2540,2543,2545,2548,2551,2553,2555,2558,2560,2562],{"class":283,"line":344},[281,2520,2521],{"class":314},"        detail ",[281,2523,350],{"class":310},[281,2525,2526],{"class":291}," \"; \"",[281,2528,2529],{"class":314},".join(",[281,2531,622],{"class":310},[281,2533,457],{"class":291},[281,2535,627],{"class":460},[281,2537,1085],{"class":314},[281,2539,633],{"class":460},[281,2541,2542],{"class":291}," at ",[281,2544,627],{"class":460},[281,2546,2547],{"class":291},"', '",[281,2549,2550],{"class":314},".join(cells)",[281,2552,633],{"class":460},[281,2554,457],{"class":291},[281,2556,2557],{"class":310}," for",[281,2559,1064],{"class":314},[281,2561,585],{"class":310},[281,2563,2564],{"class":314}," leftover.items())\n",[281,2566,2567,2570,2573,2575,2577,2580,2582,2585,2587,2589],{"class":283,"line":356},[281,2568,2569],{"class":310},"        raise",[281,2571,2572],{"class":416}," ValueError",[281,2574,591],{"class":314},[281,2576,622],{"class":310},[281,2578,2579],{"class":291},"\"unfilled placeholders: ",[281,2581,627],{"class":460},[281,2583,2584],{"class":314},"detail",[281,2586,633],{"class":460},[281,2588,457],{"class":291},[281,2590,431],{"class":314},[281,2592,2593],{"class":283,"line":367},[281,2594,341],{"emptyLinePlaceholder":340},[281,2596,2597,2600],{"class":283,"line":378},[281,2598,2599],{"class":314},"assert_filled(wb)                           ",[281,2601,2602],{"class":1271},"# raises with names and cells if anything is left\n",[281,2604,2605,2607,2610],{"class":283,"line":395},[281,2606,786],{"class":314},[281,2608,2609],{"class":291},"\"invoice_northwind.xlsx\"",[281,2611,431],{"class":314},[281,2613,2614],{"class":283,"line":434},[281,2615,341],{"emptyLinePlaceholder":340},[281,2617,2618,2621,2623,2625,2627,2629,2631],{"class":283,"line":473},[281,2619,2620],{"class":314},"saved ",[281,2622,350],{"class":310},[281,2624,1053],{"class":314},[281,2626,2609],{"class":291},[281,2628,2012],{"class":314},[281,2630,1496],{"class":291},[281,2632,2157],{"class":314},[281,2634,2635,2637,2640,2642,2645,2647,2649,2651,2654,2656,2658,2660,2663,2666],{"class":283,"line":507},[281,2636,1490],{"class":416},[281,2638,2639],{"class":314},"([saved.cell(row, ",[281,2641,99],{"class":416},[281,2643,2644],{"class":314},").value ",[281,2646,579],{"class":310},[281,2648,951],{"class":314},[281,2650,585],{"class":310},[281,2652,2653],{"class":416}," range",[281,2655,591],{"class":314},[281,2657,234],{"class":416},[281,2659,420],{"class":314},[281,2661,2662],{"class":416},"10",[281,2664,2665],{"class":314},")])   ",[281,2667,2668],{"class":1271},"# the three line items\n",[281,2670,2671,2673,2676,2679,2682],{"class":283,"line":540},[281,2672,1490],{"class":416},[281,2674,2675],{"class":314},"(saved[",[281,2677,2678],{"class":291},"\"A11\"",[281,2680,2681],{"class":314},"].value)                   ",[281,2683,2684],{"class":1271},"# closing line, moved down by the inserted rows\n",[10,2686,2687,2688,2690],{},"The check also protects against the template changing under the script: when a designer adds ",[14,2689,2479],{}," to the template next month, the first run fails with a message that\nnames the new field, instead of quietly producing invoices with a marker on them.",[267,2692,2694],{"id":2693},"common-pitfalls","Common pitfalls",[2696,2697,2698,2714],"table",{},[2699,2700,2701],"thead",{},[2702,2703,2704,2708,2711],"tr",{},[2705,2706,2707],"th",{},"Symptom",[2705,2709,2710],{},"Cause",[2705,2712,2713],{},"Fix",[2715,2716,2717,2729,2743,2756,2770,2785],"tbody",{},[2702,2718,2719,2723,2726],{},[2720,2721,2722],"td",{},"Total shows as text, currency format ignored",[2720,2724,2725],{},"Whole-cell placeholder replaced with a string",[2720,2727,2728],{},"Assign the typed value when the cell holds only the placeholder",[2702,2730,2731,2734,2737],{},[2720,2732,2733],{},"Placeholder not found",[2720,2735,2736],{},"Excel autocorrected braces, or spaces inside them",[2720,2738,2739,2740,2742],{},"Allow ",[14,2741,1108],{}," in the pattern; check the inventory output",[2702,2744,2745,2748,2753],{},[2720,2746,2747],{},"Formulas below the table point at the wrong rows",[2720,2749,2750,2752],{},[14,2751,2209],{}," does not rewrite references",[2720,2754,2755],{},"Keep totals above the table, or compute them in Python",[2702,2757,2758,2761,2764],{},[2720,2759,2760],{},"Styles missing on expanded rows",[2720,2762,2763],{},"New rows inserted without copying styles",[2720,2765,2766,2767],{},"Copy each template cell's ",[14,2768,2769],{},"_style",[2702,2771,2772,2775,2778],{},[2720,2773,2774],{},"Bold words inside a sentence lost",[2720,2776,2777],{},"Rich text flattened on load",[2720,2779,2780,2781,2784],{},"Load with ",[14,2782,2783],{},"rich_text=True"," and substitute per run",[2702,2786,2787,2790,2793],{},[2720,2788,2789],{},"Report saved with markers visible",[2720,2791,2792],{},"No post-fill check",[2720,2794,2795],{},"Scan again and raise before saving",[267,2797,2799],{"id":2798},"performance-and-scale-notes","Performance and scale notes",[10,2801,2802,2803,2806,2807,867],{},"A template scan touches every cell once, which is trivial for report-sized templates. When producing hundreds of reports from one template — one per customer — load the template\nonce, record the placeholder inventory, and for each report reload the template from bytes held in memory (",[14,2804,2805],{},"load_workbook(BytesIO(template_bytes))",") rather than from disk. Row\ninsertion is the slowest operation on large sheets, because every cell below moves; for statements with thousands of lines, a template that ends at the line-item table avoids that cost.\nLooping over many recipients is covered in\n",[27,2808,2810],{"href":2809},"\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",[267,2812,2814],{"id":2813},"conclusion","Conclusion",[10,2816,2817],{},"Placeholders let the template's designer decide where values go and how they look, and let the script find them wherever they are. Inventory the markers first, assign typed values to\nwhole-cell placeholders so formats and formulas keep working, substitute inline markers as text, expand a marked row for line items with copied styles, include headers and footers, and\nrefuse to save while any marker remains. Keep the template under version control next to the script, because the placeholders in it are now part of the program's interface.",[267,2819,2821],{"id":2820},"frequently-asked-questions","Frequently asked questions",[10,2823,2824,2828],{},[2825,2826,2827],"strong",{},"Can I use Jinja2 syntax in Excel templates?","\nThe double-brace markers look like Jinja2, and the approach is similar, but Excel cells are not a text document, so a small openpyxl filler like this one is simpler than rendering the workbook XML with a template engine.",[10,2830,2831,2834],{},[2825,2832,2833],{},"Will placeholders inside formulas be replaced?","\nNot by this code, which treats formula cells like any other string only if they contain a marker. Put placeholders in value cells and let formulas refer to those cells instead.",[10,2836,2837,2840],{},[2825,2838,2839],{},"How do I fill a placeholder with a date?","\nPass a datetime or date object for a whole-cell placeholder, and give that cell a date number format in the template. For inline text, format the date as a string first.",[10,2842,2843,2846],{},[2825,2844,2845],{},"Can the same placeholder appear in several places?","\nYes. Every occurrence is replaced, which is useful for a customer name on a cover sheet, in a header and in a closing sentence.",[267,2848,2850],{"id":2849},"related","Related",[2852,2853,2854,2861,2868,2875,2882],"ul",{},[2855,2856,2857,2858,2860],"li",{},"Up one level: ",[27,2859,30],{"href":29}," — template-driven reporting end to end.",[2855,2862,2863,2867],{},[27,2864,2866],{"href":2865},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Ffill-excel-template-with-python-openpyxl\u002F","Fill an Excel Template with Python and openpyxl"," — the named-cell alternative to placeholders.",[2855,2869,2870,2874],{},[27,2871,2873],{"href":2872},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fpopulate-excel-template-without-losing-formatting\u002F","Populate an Excel Template Without Losing Formatting"," — what survives a template fill.",[2855,2876,2877,2881],{},[27,2878,2880],{"href":2879},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Ffind-and-replace-text-in-excel-with-openpyxl\u002F","Find and Replace Text in Excel with openpyxl"," — rich text and formulas during replacement.",[2855,2883,2884,2888],{},[27,2885,2887],{"href":2886},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fkeep-charts-and-images-when-filling-an-excel-template\u002F","Keep Charts and Images When Filling an Excel Template"," — templates with pictures and charts.",[2890,2891,2892],"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}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":277,"searchDepth":324,"depth":324,"links":2894},[2895,2896,2897,2898,2899,2900,2901,2902,2903,2904,2905],{"id":269,"depth":324,"text":270},{"id":794,"depth":324,"text":795},{"id":1119,"depth":324,"text":1120},{"id":1636,"depth":324,"text":1637},{"id":2224,"depth":324,"text":2225},{"id":2472,"depth":324,"text":2473},{"id":2693,"depth":324,"text":2694},{"id":2798,"depth":324,"text":2799},{"id":2813,"depth":324,"text":2814},{"id":2820,"depth":324,"text":2821},{"id":2849,"depth":324,"text":2850},"2026-09-17","Let report designers type named markers such as customer and period straight into the template, then fill them from Python: whole-cell placeholders that keep numbers numeric, placeholders inside text, a repeating row for line items, and checks for anything left unfilled.","md",[2910,2912,2914,2916],{"q":2827,"a":2911},"The double-brace markers look like Jinja2, and the approach is similar, but Excel cells are not a text document, so a small openpyxl filler like this one is simpler than rendering the workbook XML with a template engine.",{"q":2833,"a":2913},"Not by this code, which treats formula cells like any other string only if they contain a marker. Put placeholders in value cells and let formulas refer to those cells instead.",{"q":2839,"a":2915},"Pass a datetime or date object for a whole-cell placeholder, and give that cell a date number format in the template. For inline text, format the date as a string first.",{"q":2845,"a":2917},"Yes. Every occurrence is replaced, which is useful for a customer name on a cover sheet, in a header and in a closing sentence.","replace placeholders in an excel template with python",null,{"breadcrumb":2921},[2922,2925,2928],{"name":2923,"item":2924},"Home","\u002F",{"name":2926,"item":2927},"Automating Reporting Workflows","\u002Fautomating-reporting-workflows\u002F",{"name":30,"item":29},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Freplace-placeholders-in-an-excel-template-with-python",{"title":5,"description":2931},"Fill Excel templates by placeholder with openpyxl: typed whole-cell values, placeholders inside sentences, repeating line-item rows with copied styles, headers and footers, and unfilled-placeholder checks.","replace-placeholders-in-an-excel-template-with-python","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Freplace-placeholders-in-an-excel-template-with-python\u002Findex",[2935],"report-layouts","how-to","TCBZo3kZsOIq3sfm-Lj-ldVswRU8BE3gREVy4N7iTEQ",[2939,2943],{"title":2940,"path":2941,"stem":2942,"children":-1},"Repeat a Template Block for Each Record in an Excel Report","\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Frepeat-a-template-block-for-each-record","automating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Frepeat-a-template-block-for-each-record\u002Findex",{"title":2944,"path":2945,"stem":2946,"children":-1},"Publishing Excel Reports to Cloud Storage","\u002Fautomating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage","automating-reporting-workflows\u002Fpublishing-excel-reports-to-cloud-storage\u002Findex",[2948,2950],{"path":2949,"title":30},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates",{"path":2951,"title":2952},"\u002Fautomating-reporting-workflows","Automating Reporting Workflows with Python and Excel",[],[2955,2959,2962,2965,2968,2971],{"path":2956,"title":2957,"kind":2958},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports","Building Finance and Operations Reports in Excel with Python","guide",{"path":2960,"title":2961,"kind":2936},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fallocate-shared-costs-across-departments","Allocate Shared Costs Across Departments with Python",{"path":2963,"title":2964,"kind":2936},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bank-reconciliation-report","Build a Bank Reconciliation Report with Python and Excel",{"path":2966,"title":2967,"kind":2936},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-bill-of-materials-cost-rollup","Build a Bill of Materials Cost Rollup",{"path":2969,"title":2970,"kind":2936},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-break-even-analysis-in-excel-with-python","Build a Break-Even Analysis in Excel with Python",{"path":2972,"title":2973,"kind":2974},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fbuild-a-budget-vs-actual-variance-report","Build a Budget vs Actual Variance Report in Excel with Python","compare",1790972138283]