[{"data":1,"prerenderedAt":4166},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fapply-a-reusable-style-theme-across-an-excel-report":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fapply-a-reusable-style-theme-across-an-excel-report":4158},{"id":4,"title":5,"body":6,"dateModified":4133,"datePublished":4133,"description":4134,"extension":4135,"faq":4136,"meta":4149,"navigation":400,"path":4150,"seo":4151,"slug":4154,"stem":4155,"type":4156,"__hash__":4157},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fapply-a-reusable-style-theme-across-an-excel-report\u002Findex.md","Apply a Reusable Style Theme Across an Excel Report",{"type":7,"value":8,"toc":4120},"minimark",[9,24,188,193,221,242,346,350,353,1776,1787,1791,1872,1883,1886,2014,2018,2021,2362,2369,2372,2616,2636,2640,2643,2716,2903,2906,2910,2978,2985,3249,3252,3376,3387,3391,3516,3520,3523,3530,3539,3599,3609,3991,4010,4014,4020,4024,4036,4049,4061,4067,4073,4077,4117],[10,11,12,13,17,18,23],"p",{},"The third report you write with openpyxl has the same header format as the first two, typed out again slightly differently. By the tenth, no two reports look alike, and changing the brand colour means finding every ",[14,15,16],"code",{},"PatternFill"," in the codebase. A theme fixes that: define each visual role once — header, total, currency, warning — register them on the workbook as named styles, and apply them by name. This guide builds one, covers the format-ceiling error a per-cell approach eventually hits, and shows how to share the theme across reports. It extends ",[19,20,22],"a",{"href":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","Styling Excel Cells with openpyxl",".",[25,26,35,36,35,40,35,44,35,51,35,60,35,67,35,76,35,81,35,84,35,89,35,92,35,96,35,101,35,106,35,111,35,116,35,131,35,144,35,151,35,157,35,162,35,165,35,170,35,174,35,180,35,184],"svg",{"viewBox":27,"role":28,"ariaLabel":29,"ariaLabelledBy":30,"xmlns":33,"style":34},"0 0 800 260","img","A theme of named styles applied by role: one definition each for header, body, currency, total and warning, referenced from any number of cells across any number of reports.",[31,32],"theme-t","theme-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[37,38,39],"title",{"id":31},"One definition per role, referenced everywhere",[41,42,43],"desc",{"id":32},"Five named styles — header, body, currency, total and warning — are defined once in a theme module. Each is registered on the workbook a single time and referenced by name from any number of cells, across any number of reports. Changing a brand colour means editing one definition rather than finding every scattered fill assignment, and Excel stores each style once regardless of how many cells use it.",[45,46],"rect",{"x":47,"y":47,"width":48,"height":49,"fill":50},"0","800","260","#ffffff",[45,52],{"x":53,"y":54,"width":55,"height":56,"rx":53,"fill":57,"stroke":58,"style":59},"14","34","230","212","#f0f2f5","var(--line,#cdd5e6)","stroke-width:2px",[61,62,66],"text",{"x":63,"y":64,"style":65},"129","60","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","theme.py — defined once",[45,68],{"x":69,"y":70,"width":71,"height":72,"rx":73,"fill":74,"stroke":75,"style":59},"36","74","186","28","6","#ebebfd","var(--brand,#5b5cf0)",[61,77,80],{"x":63,"y":78,"style":79},"93","font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","header",[45,82],{"x":69,"y":83,"width":71,"height":72,"rx":73,"fill":50,"stroke":58},"108",[61,85,88],{"x":63,"y":86,"style":87},"127","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","body",[45,90],{"x":69,"y":91,"width":71,"height":72,"rx":73,"fill":50,"stroke":58},"142",[61,93,95],{"x":63,"y":94,"style":87},"161","currency",[45,97],{"x":69,"y":98,"width":71,"height":72,"rx":73,"fill":99,"stroke":100,"style":59},"176","#d9f4f1","var(--teal,#0f9488)",[61,102,105],{"x":63,"y":103,"style":104},"195","font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","total",[45,107],{"x":69,"y":108,"width":71,"height":72,"rx":73,"fill":109,"stroke":110,"style":59},"210","#fee8f2","var(--accent,#f43f8f)",[61,112,115],{"x":63,"y":113,"style":114},"229","font-size:10.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","warning",[117,118,120,121,120,125,120,128,35],"g",{"stroke":75,"style":59,"fill":119},"none","\n    ",[122,123],"path",{"d":124},"M244 96 H 284 V 84 H 318",[122,126],{"d":127},"M244 140 H 284 V 152 H 318",[122,129],{"d":130},"M244 190 H 284 V 220 H 318",[117,132,120,134,120,138,120,141,35],{"fill":133},"#5b5cf0",[135,136],"polygon",{"points":137},"326,84 314,78 314,90",[135,139],{"points":140},"326,152 314,146 314,158",[135,142],{"points":143},"326,220 314,214 314,226",[45,145],{"x":146,"y":147,"width":148,"height":149,"rx":150,"fill":74,"stroke":75,"style":59},"334","52","450","64","12",[61,152,156],{"x":153,"y":154,"style":155},"358","76","font-size:11.5px;font-weight:700;fill:var(--brand-strong,#4338ca)","regional_report.xlsx",[61,158,161],{"x":153,"y":159,"style":160},"98","font-size:10.5px;fill:var(--muted,#5b6780)","cell.style = \"header\" · \"currency\" · \"total\"",[45,163],{"x":146,"y":164,"width":148,"height":147,"rx":150,"fill":99,"stroke":100,"style":59},"126",[61,166,169],{"x":153,"y":167,"style":168},"148","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157)","monthly_summary.xlsx",[61,171,173],{"x":153,"y":172,"style":160},"168","the same names, the same look",[45,175],{"x":146,"y":176,"width":148,"height":177,"rx":150,"fill":178,"stroke":179,"style":59},"188","58","#fdefd8","var(--gold,#b4740a)",[61,181,183],{"x":153,"y":56,"style":182},"font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06)","change one colour in theme.py",[61,185,187],{"x":153,"y":186,"style":160},"232","and every report follows",[189,190,192],"h2",{"id":191},"prerequisites","Prerequisites",[194,195,200],"pre",{"className":196,"code":197,"language":198,"meta":199,"style":199},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash","",[14,201,202],{"__ignoreMap":199},[203,204,207,211,215,218],"span",{"class":205,"line":206},"line",1,[203,208,210],{"class":209},"sMTad","pip",[203,212,214],{"class":213},"srMev"," install",[203,216,217],{"class":213}," openpyxl",[203,219,220],{"class":213}," pandas\n",[10,222,223,224,228,229,231,232,234,235,231,238,241],{},"Before writing any code, decide the ",[225,226,227],"strong",{},"roles"," rather than the appearances. A theme keyed on ",[14,230,80],{}," and ",[14,233,105],{}," survives a redesign; one keyed on ",[14,236,237],{},"indigo_bold",[14,239,240],{},"green_fill"," does not.",[243,244,245,258],"table",{},[246,247,248],"thead",{},[249,250,251,255],"tr",{},[252,253,254],"th",{},"Role",[252,256,257],{},"Used for",[259,260,261,271,281,290,299,308,318,328,337],"tbody",{},[249,262,263,268],{},[264,265,266],"td",{},[14,267,37],{},[264,269,270],{},"the report banner",[249,272,273,278],{},[264,274,275],{},[14,276,277],{},"subtitle",[264,279,280],{},"period and generation stamp",[249,282,283,287],{},[264,284,285],{},[14,286,80],{},[264,288,289],{},"column headings",[249,291,292,296],{},[264,293,294],{},[14,295,88],{},[264,297,298],{},"ordinary data cells",[249,300,301,305],{},[264,302,303],{},[14,304,95],{},[264,306,307],{},"money columns",[249,309,310,315],{},[264,311,312],{},[14,313,314],{},"percent",[264,316,317],{},"rate columns",[249,319,320,325],{},[264,321,322],{},[14,323,324],{},"date",[264,326,327],{},"date columns",[249,329,330,334],{},[264,331,332],{},[14,333,105],{},[264,335,336],{},"summary rows",[249,338,339,343],{},[264,340,341],{},[14,342,115],{},[264,344,345],{},"values needing attention",[189,347,349],{"id":348},"step-1-define-the-palette-and-the-roles","Step 1 — Define the palette and the roles",[10,351,352],{},"Keep the colours in one place, and build the styles from them:",[194,354,358],{"className":355,"code":356,"language":357,"meta":199,"style":199},"language-python shiki shiki-themes github-light github-dark-high-contrast","# theme.py\nfrom openpyxl.styles import (\n    Alignment, Border, Font, NamedStyle, PatternFill, Side\n)\n\nPALETTE = {\n    \"brand\": \"4338CA\",\n    \"brand_soft\": \"EBEBFD\",\n    \"ink\": \"172033\",\n    \"muted\": \"5B6780\",\n    \"line\": \"CDD5E6\",\n    \"ok\": \"0B6157\",\n    \"ok_soft\": \"D9F4F1\",\n    \"warn\": \"BE185D\",\n    \"warn_soft\": \"FEE8F2\",\n    \"white\": \"FFFFFF\",\n}\n\nFONT = \"Calibri\"\nTHIN = Side(style=\"thin\", color=PALETTE[\"line\"])\n\n\ndef build_styles():\n    \"\"\"Return the report theme as a list of NamedStyle objects.\"\"\"\n    title = NamedStyle(name=\"title\")\n    title.font = Font(name=FONT, size=15, bold=True, color=PALETTE[\"brand\"])\n    title.fill = PatternFill(\"solid\", start_color=PALETTE[\"brand_soft\"],\n                             end_color=PALETTE[\"brand_soft\"])\n    title.alignment = Alignment(horizontal=\"center\", vertical=\"center\")\n\n    subtitle = NamedStyle(name=\"subtitle\")\n    subtitle.font = Font(name=FONT, size=9, italic=True, color=PALETTE[\"muted\"])\n    subtitle.alignment = Alignment(horizontal=\"center\", vertical=\"center\")\n\n    header = NamedStyle(name=\"header\")\n    header.font = Font(name=FONT, size=11, bold=True, color=PALETTE[\"white\"])\n    header.fill = PatternFill(\"solid\", start_color=PALETTE[\"brand\"],\n                              end_color=PALETTE[\"brand\"])\n    header.alignment = Alignment(horizontal=\"center\", vertical=\"center\",\n                                 wrap_text=True)\n    header.border = Border(bottom=THIN)\n\n    body = NamedStyle(name=\"body\")\n    body.font = Font(name=FONT, size=11, color=PALETTE[\"ink\"])\n    body.alignment = Alignment(vertical=\"center\")\n\n    currency = NamedStyle(name=\"currency\")\n    currency.font = Font(name=FONT, size=11, color=PALETTE[\"ink\"])\n    currency.number_format = \"#,##0.00\"\n    currency.alignment = Alignment(horizontal=\"right\", vertical=\"center\")\n\n    percent = NamedStyle(name=\"percent\")\n    percent.font = Font(name=FONT, size=11, color=PALETTE[\"ink\"])\n    percent.number_format = \"0.0%\"\n    percent.alignment = Alignment(horizontal=\"right\", vertical=\"center\")\n\n    date = NamedStyle(name=\"date\")\n    date.font = Font(name=FONT, size=11, color=PALETTE[\"ink\"])\n    date.number_format = \"yyyy-mm-dd\"\n    date.alignment = Alignment(horizontal=\"center\", vertical=\"center\")\n\n    total = NamedStyle(name=\"total\")\n    total.font = Font(name=FONT, size=11, bold=True, color=PALETTE[\"ok\"])\n    total.fill = PatternFill(\"solid\", start_color=PALETTE[\"ok_soft\"],\n                             end_color=PALETTE[\"ok_soft\"])\n    total.number_format = \"#,##0.00\"\n    total.border = Border(top=THIN)\n    total.alignment = Alignment(horizontal=\"right\", vertical=\"center\")\n\n    warning = NamedStyle(name=\"warning\")\n    warning.font = Font(name=FONT, size=11, bold=True, color=PALETTE[\"warn\"])\n    warning.fill = PatternFill(\"solid\", start_color=PALETTE[\"warn_soft\"],\n                               end_color=PALETTE[\"warn_soft\"])\n    warning.number_format = \"#,##0.00\"\n    warning.alignment = Alignment(horizontal=\"right\", vertical=\"center\")\n\n    return [title, subtitle, header, body, currency, percent, date,\n            total, warning]\n","python",[14,359,360,366,383,389,395,402,415,430,443,456,469,482,495,508,521,534,547,553,558,569,609,614,619,632,638,659,711,742,758,788,793,812,861,887,892,911,959,985,1001,1027,1039,1059,1064,1083,1122,1140,1145,1164,1202,1213,1240,1245,1264,1302,1313,1339,1344,1363,1401,1412,1438,1443,1462,1509,1536,1551,1561,1580,1606,1611,1630,1677,1704,1720,1730,1756,1761,1770],{"__ignoreMap":199},[203,361,362],{"class":205,"line":206},[203,363,365],{"class":364},"s-wDw","# theme.py\n",[203,367,369,373,377,380],{"class":205,"line":368},2,[203,370,372],{"class":371},"s-kum","from",[203,374,376],{"class":375},"skGVy"," openpyxl.styles ",[203,378,379],{"class":371},"import",[203,381,382],{"class":375}," (\n",[203,384,386],{"class":205,"line":385},3,[203,387,388],{"class":375},"    Alignment, Border, Font, NamedStyle, PatternFill, Side\n",[203,390,392],{"class":205,"line":391},4,[203,393,394],{"class":375},")\n",[203,396,398],{"class":205,"line":397},5,[203,399,401],{"emptyLinePlaceholder":400},true,"\n",[203,403,405,409,412],{"class":205,"line":404},6,[203,406,408],{"class":407},"sP0c6","PALETTE",[203,410,411],{"class":371}," =",[203,413,414],{"class":375}," {\n",[203,416,418,421,424,427],{"class":205,"line":417},7,[203,419,420],{"class":213},"    \"brand\"",[203,422,423],{"class":375},": ",[203,425,426],{"class":213},"\"4338CA\"",[203,428,429],{"class":375},",\n",[203,431,433,436,438,441],{"class":205,"line":432},8,[203,434,435],{"class":213},"    \"brand_soft\"",[203,437,423],{"class":375},[203,439,440],{"class":213},"\"EBEBFD\"",[203,442,429],{"class":375},[203,444,446,449,451,454],{"class":205,"line":445},9,[203,447,448],{"class":213},"    \"ink\"",[203,450,423],{"class":375},[203,452,453],{"class":213},"\"172033\"",[203,455,429],{"class":375},[203,457,459,462,464,467],{"class":205,"line":458},10,[203,460,461],{"class":213},"    \"muted\"",[203,463,423],{"class":375},[203,465,466],{"class":213},"\"5B6780\"",[203,468,429],{"class":375},[203,470,472,475,477,480],{"class":205,"line":471},11,[203,473,474],{"class":213},"    \"line\"",[203,476,423],{"class":375},[203,478,479],{"class":213},"\"CDD5E6\"",[203,481,429],{"class":375},[203,483,485,488,490,493],{"class":205,"line":484},12,[203,486,487],{"class":213},"    \"ok\"",[203,489,423],{"class":375},[203,491,492],{"class":213},"\"0B6157\"",[203,494,429],{"class":375},[203,496,498,501,503,506],{"class":205,"line":497},13,[203,499,500],{"class":213},"    \"ok_soft\"",[203,502,423],{"class":375},[203,504,505],{"class":213},"\"D9F4F1\"",[203,507,429],{"class":375},[203,509,511,514,516,519],{"class":205,"line":510},14,[203,512,513],{"class":213},"    \"warn\"",[203,515,423],{"class":375},[203,517,518],{"class":213},"\"BE185D\"",[203,520,429],{"class":375},[203,522,524,527,529,532],{"class":205,"line":523},15,[203,525,526],{"class":213},"    \"warn_soft\"",[203,528,423],{"class":375},[203,530,531],{"class":213},"\"FEE8F2\"",[203,533,429],{"class":375},[203,535,537,540,542,545],{"class":205,"line":536},16,[203,538,539],{"class":213},"    \"white\"",[203,541,423],{"class":375},[203,543,544],{"class":213},"\"FFFFFF\"",[203,546,429],{"class":375},[203,548,550],{"class":205,"line":549},17,[203,551,552],{"class":375},"}\n",[203,554,556],{"class":205,"line":555},18,[203,557,401],{"emptyLinePlaceholder":400},[203,559,561,564,566],{"class":205,"line":560},19,[203,562,563],{"class":407},"FONT",[203,565,411],{"class":371},[203,567,568],{"class":213}," \"Calibri\"\n",[203,570,572,575,577,580,584,587,590,593,596,598,600,603,606],{"class":205,"line":571},20,[203,573,574],{"class":407},"THIN",[203,576,411],{"class":371},[203,578,579],{"class":375}," Side(",[203,581,583],{"class":582},"sa561","style",[203,585,586],{"class":371},"=",[203,588,589],{"class":213},"\"thin\"",[203,591,592],{"class":375},", ",[203,594,595],{"class":582},"color",[203,597,586],{"class":371},[203,599,408],{"class":407},[203,601,602],{"class":375},"[",[203,604,605],{"class":213},"\"line\"",[203,607,608],{"class":375},"])\n",[203,610,612],{"class":205,"line":611},21,[203,613,401],{"emptyLinePlaceholder":400},[203,615,617],{"class":205,"line":616},22,[203,618,401],{"emptyLinePlaceholder":400},[203,620,622,625,629],{"class":205,"line":621},23,[203,623,624],{"class":371},"def",[203,626,628],{"class":627},"s_Opv"," build_styles",[203,630,631],{"class":375},"():\n",[203,633,635],{"class":205,"line":634},24,[203,636,637],{"class":213},"    \"\"\"Return the report theme as a list of NamedStyle objects.\"\"\"\n",[203,639,641,644,646,649,652,654,657],{"class":205,"line":640},25,[203,642,643],{"class":375},"    title ",[203,645,586],{"class":371},[203,647,648],{"class":375}," NamedStyle(",[203,650,651],{"class":582},"name",[203,653,586],{"class":371},[203,655,656],{"class":213},"\"title\"",[203,658,394],{"class":375},[203,660,662,665,667,670,672,674,676,678,681,683,686,688,691,693,696,698,700,702,704,706,709],{"class":205,"line":661},26,[203,663,664],{"class":375},"    title.font ",[203,666,586],{"class":371},[203,668,669],{"class":375}," Font(",[203,671,651],{"class":582},[203,673,586],{"class":371},[203,675,563],{"class":407},[203,677,592],{"class":375},[203,679,680],{"class":582},"size",[203,682,586],{"class":371},[203,684,685],{"class":407},"15",[203,687,592],{"class":375},[203,689,690],{"class":582},"bold",[203,692,586],{"class":371},[203,694,695],{"class":407},"True",[203,697,592],{"class":375},[203,699,595],{"class":582},[203,701,586],{"class":371},[203,703,408],{"class":407},[203,705,602],{"class":375},[203,707,708],{"class":213},"\"brand\"",[203,710,608],{"class":375},[203,712,714,717,719,722,725,727,730,732,734,736,739],{"class":205,"line":713},27,[203,715,716],{"class":375},"    title.fill ",[203,718,586],{"class":371},[203,720,721],{"class":375}," PatternFill(",[203,723,724],{"class":213},"\"solid\"",[203,726,592],{"class":375},[203,728,729],{"class":582},"start_color",[203,731,586],{"class":371},[203,733,408],{"class":407},[203,735,602],{"class":375},[203,737,738],{"class":213},"\"brand_soft\"",[203,740,741],{"class":375},"],\n",[203,743,745,748,750,752,754,756],{"class":205,"line":744},28,[203,746,747],{"class":582},"                             end_color",[203,749,586],{"class":371},[203,751,408],{"class":407},[203,753,602],{"class":375},[203,755,738],{"class":213},[203,757,608],{"class":375},[203,759,761,764,766,769,772,774,777,779,782,784,786],{"class":205,"line":760},29,[203,762,763],{"class":375},"    title.alignment ",[203,765,586],{"class":371},[203,767,768],{"class":375}," Alignment(",[203,770,771],{"class":582},"horizontal",[203,773,586],{"class":371},[203,775,776],{"class":213},"\"center\"",[203,778,592],{"class":375},[203,780,781],{"class":582},"vertical",[203,783,586],{"class":371},[203,785,776],{"class":213},[203,787,394],{"class":375},[203,789,791],{"class":205,"line":790},30,[203,792,401],{"emptyLinePlaceholder":400},[203,794,796,799,801,803,805,807,810],{"class":205,"line":795},31,[203,797,798],{"class":375},"    subtitle ",[203,800,586],{"class":371},[203,802,648],{"class":375},[203,804,651],{"class":582},[203,806,586],{"class":371},[203,808,809],{"class":213},"\"subtitle\"",[203,811,394],{"class":375},[203,813,815,818,820,822,824,826,828,830,832,834,837,839,842,844,846,848,850,852,854,856,859],{"class":205,"line":814},32,[203,816,817],{"class":375},"    subtitle.font ",[203,819,586],{"class":371},[203,821,669],{"class":375},[203,823,651],{"class":582},[203,825,586],{"class":371},[203,827,563],{"class":407},[203,829,592],{"class":375},[203,831,680],{"class":582},[203,833,586],{"class":371},[203,835,836],{"class":407},"9",[203,838,592],{"class":375},[203,840,841],{"class":582},"italic",[203,843,586],{"class":371},[203,845,695],{"class":407},[203,847,592],{"class":375},[203,849,595],{"class":582},[203,851,586],{"class":371},[203,853,408],{"class":407},[203,855,602],{"class":375},[203,857,858],{"class":213},"\"muted\"",[203,860,608],{"class":375},[203,862,864,867,869,871,873,875,877,879,881,883,885],{"class":205,"line":863},33,[203,865,866],{"class":375},"    subtitle.alignment ",[203,868,586],{"class":371},[203,870,768],{"class":375},[203,872,771],{"class":582},[203,874,586],{"class":371},[203,876,776],{"class":213},[203,878,592],{"class":375},[203,880,781],{"class":582},[203,882,586],{"class":371},[203,884,776],{"class":213},[203,886,394],{"class":375},[203,888,890],{"class":205,"line":889},34,[203,891,401],{"emptyLinePlaceholder":400},[203,893,895,898,900,902,904,906,909],{"class":205,"line":894},35,[203,896,897],{"class":375},"    header ",[203,899,586],{"class":371},[203,901,648],{"class":375},[203,903,651],{"class":582},[203,905,586],{"class":371},[203,907,908],{"class":213},"\"header\"",[203,910,394],{"class":375},[203,912,914,917,919,921,923,925,927,929,931,933,936,938,940,942,944,946,948,950,952,954,957],{"class":205,"line":913},36,[203,915,916],{"class":375},"    header.font ",[203,918,586],{"class":371},[203,920,669],{"class":375},[203,922,651],{"class":582},[203,924,586],{"class":371},[203,926,563],{"class":407},[203,928,592],{"class":375},[203,930,680],{"class":582},[203,932,586],{"class":371},[203,934,935],{"class":407},"11",[203,937,592],{"class":375},[203,939,690],{"class":582},[203,941,586],{"class":371},[203,943,695],{"class":407},[203,945,592],{"class":375},[203,947,595],{"class":582},[203,949,586],{"class":371},[203,951,408],{"class":407},[203,953,602],{"class":375},[203,955,956],{"class":213},"\"white\"",[203,958,608],{"class":375},[203,960,962,965,967,969,971,973,975,977,979,981,983],{"class":205,"line":961},37,[203,963,964],{"class":375},"    header.fill ",[203,966,586],{"class":371},[203,968,721],{"class":375},[203,970,724],{"class":213},[203,972,592],{"class":375},[203,974,729],{"class":582},[203,976,586],{"class":371},[203,978,408],{"class":407},[203,980,602],{"class":375},[203,982,708],{"class":213},[203,984,741],{"class":375},[203,986,988,991,993,995,997,999],{"class":205,"line":987},38,[203,989,990],{"class":582},"                              end_color",[203,992,586],{"class":371},[203,994,408],{"class":407},[203,996,602],{"class":375},[203,998,708],{"class":213},[203,1000,608],{"class":375},[203,1002,1004,1007,1009,1011,1013,1015,1017,1019,1021,1023,1025],{"class":205,"line":1003},39,[203,1005,1006],{"class":375},"    header.alignment ",[203,1008,586],{"class":371},[203,1010,768],{"class":375},[203,1012,771],{"class":582},[203,1014,586],{"class":371},[203,1016,776],{"class":213},[203,1018,592],{"class":375},[203,1020,781],{"class":582},[203,1022,586],{"class":371},[203,1024,776],{"class":213},[203,1026,429],{"class":375},[203,1028,1030,1033,1035,1037],{"class":205,"line":1029},40,[203,1031,1032],{"class":582},"                                 wrap_text",[203,1034,586],{"class":371},[203,1036,695],{"class":407},[203,1038,394],{"class":375},[203,1040,1042,1045,1047,1050,1053,1055,1057],{"class":205,"line":1041},41,[203,1043,1044],{"class":375},"    header.border ",[203,1046,586],{"class":371},[203,1048,1049],{"class":375}," Border(",[203,1051,1052],{"class":582},"bottom",[203,1054,586],{"class":371},[203,1056,574],{"class":407},[203,1058,394],{"class":375},[203,1060,1062],{"class":205,"line":1061},42,[203,1063,401],{"emptyLinePlaceholder":400},[203,1065,1067,1070,1072,1074,1076,1078,1081],{"class":205,"line":1066},43,[203,1068,1069],{"class":375},"    body ",[203,1071,586],{"class":371},[203,1073,648],{"class":375},[203,1075,651],{"class":582},[203,1077,586],{"class":371},[203,1079,1080],{"class":213},"\"body\"",[203,1082,394],{"class":375},[203,1084,1086,1089,1091,1093,1095,1097,1099,1101,1103,1105,1107,1109,1111,1113,1115,1117,1120],{"class":205,"line":1085},44,[203,1087,1088],{"class":375},"    body.font ",[203,1090,586],{"class":371},[203,1092,669],{"class":375},[203,1094,651],{"class":582},[203,1096,586],{"class":371},[203,1098,563],{"class":407},[203,1100,592],{"class":375},[203,1102,680],{"class":582},[203,1104,586],{"class":371},[203,1106,935],{"class":407},[203,1108,592],{"class":375},[203,1110,595],{"class":582},[203,1112,586],{"class":371},[203,1114,408],{"class":407},[203,1116,602],{"class":375},[203,1118,1119],{"class":213},"\"ink\"",[203,1121,608],{"class":375},[203,1123,1125,1128,1130,1132,1134,1136,1138],{"class":205,"line":1124},45,[203,1126,1127],{"class":375},"    body.alignment ",[203,1129,586],{"class":371},[203,1131,768],{"class":375},[203,1133,781],{"class":582},[203,1135,586],{"class":371},[203,1137,776],{"class":213},[203,1139,394],{"class":375},[203,1141,1143],{"class":205,"line":1142},46,[203,1144,401],{"emptyLinePlaceholder":400},[203,1146,1148,1151,1153,1155,1157,1159,1162],{"class":205,"line":1147},47,[203,1149,1150],{"class":375},"    currency ",[203,1152,586],{"class":371},[203,1154,648],{"class":375},[203,1156,651],{"class":582},[203,1158,586],{"class":371},[203,1160,1161],{"class":213},"\"currency\"",[203,1163,394],{"class":375},[203,1165,1167,1170,1172,1174,1176,1178,1180,1182,1184,1186,1188,1190,1192,1194,1196,1198,1200],{"class":205,"line":1166},48,[203,1168,1169],{"class":375},"    currency.font ",[203,1171,586],{"class":371},[203,1173,669],{"class":375},[203,1175,651],{"class":582},[203,1177,586],{"class":371},[203,1179,563],{"class":407},[203,1181,592],{"class":375},[203,1183,680],{"class":582},[203,1185,586],{"class":371},[203,1187,935],{"class":407},[203,1189,592],{"class":375},[203,1191,595],{"class":582},[203,1193,586],{"class":371},[203,1195,408],{"class":407},[203,1197,602],{"class":375},[203,1199,1119],{"class":213},[203,1201,608],{"class":375},[203,1203,1205,1208,1210],{"class":205,"line":1204},49,[203,1206,1207],{"class":375},"    currency.number_format ",[203,1209,586],{"class":371},[203,1211,1212],{"class":213}," \"#,##0.00\"\n",[203,1214,1216,1219,1221,1223,1225,1227,1230,1232,1234,1236,1238],{"class":205,"line":1215},50,[203,1217,1218],{"class":375},"    currency.alignment ",[203,1220,586],{"class":371},[203,1222,768],{"class":375},[203,1224,771],{"class":582},[203,1226,586],{"class":371},[203,1228,1229],{"class":213},"\"right\"",[203,1231,592],{"class":375},[203,1233,781],{"class":582},[203,1235,586],{"class":371},[203,1237,776],{"class":213},[203,1239,394],{"class":375},[203,1241,1243],{"class":205,"line":1242},51,[203,1244,401],{"emptyLinePlaceholder":400},[203,1246,1248,1251,1253,1255,1257,1259,1262],{"class":205,"line":1247},52,[203,1249,1250],{"class":375},"    percent ",[203,1252,586],{"class":371},[203,1254,648],{"class":375},[203,1256,651],{"class":582},[203,1258,586],{"class":371},[203,1260,1261],{"class":213},"\"percent\"",[203,1263,394],{"class":375},[203,1265,1267,1270,1272,1274,1276,1278,1280,1282,1284,1286,1288,1290,1292,1294,1296,1298,1300],{"class":205,"line":1266},53,[203,1268,1269],{"class":375},"    percent.font ",[203,1271,586],{"class":371},[203,1273,669],{"class":375},[203,1275,651],{"class":582},[203,1277,586],{"class":371},[203,1279,563],{"class":407},[203,1281,592],{"class":375},[203,1283,680],{"class":582},[203,1285,586],{"class":371},[203,1287,935],{"class":407},[203,1289,592],{"class":375},[203,1291,595],{"class":582},[203,1293,586],{"class":371},[203,1295,408],{"class":407},[203,1297,602],{"class":375},[203,1299,1119],{"class":213},[203,1301,608],{"class":375},[203,1303,1305,1308,1310],{"class":205,"line":1304},54,[203,1306,1307],{"class":375},"    percent.number_format ",[203,1309,586],{"class":371},[203,1311,1312],{"class":213}," \"0.0%\"\n",[203,1314,1316,1319,1321,1323,1325,1327,1329,1331,1333,1335,1337],{"class":205,"line":1315},55,[203,1317,1318],{"class":375},"    percent.alignment ",[203,1320,586],{"class":371},[203,1322,768],{"class":375},[203,1324,771],{"class":582},[203,1326,586],{"class":371},[203,1328,1229],{"class":213},[203,1330,592],{"class":375},[203,1332,781],{"class":582},[203,1334,586],{"class":371},[203,1336,776],{"class":213},[203,1338,394],{"class":375},[203,1340,1342],{"class":205,"line":1341},56,[203,1343,401],{"emptyLinePlaceholder":400},[203,1345,1347,1350,1352,1354,1356,1358,1361],{"class":205,"line":1346},57,[203,1348,1349],{"class":375},"    date ",[203,1351,586],{"class":371},[203,1353,648],{"class":375},[203,1355,651],{"class":582},[203,1357,586],{"class":371},[203,1359,1360],{"class":213},"\"date\"",[203,1362,394],{"class":375},[203,1364,1366,1369,1371,1373,1375,1377,1379,1381,1383,1385,1387,1389,1391,1393,1395,1397,1399],{"class":205,"line":1365},58,[203,1367,1368],{"class":375},"    date.font ",[203,1370,586],{"class":371},[203,1372,669],{"class":375},[203,1374,651],{"class":582},[203,1376,586],{"class":371},[203,1378,563],{"class":407},[203,1380,592],{"class":375},[203,1382,680],{"class":582},[203,1384,586],{"class":371},[203,1386,935],{"class":407},[203,1388,592],{"class":375},[203,1390,595],{"class":582},[203,1392,586],{"class":371},[203,1394,408],{"class":407},[203,1396,602],{"class":375},[203,1398,1119],{"class":213},[203,1400,608],{"class":375},[203,1402,1404,1407,1409],{"class":205,"line":1403},59,[203,1405,1406],{"class":375},"    date.number_format ",[203,1408,586],{"class":371},[203,1410,1411],{"class":213}," \"yyyy-mm-dd\"\n",[203,1413,1415,1418,1420,1422,1424,1426,1428,1430,1432,1434,1436],{"class":205,"line":1414},60,[203,1416,1417],{"class":375},"    date.alignment ",[203,1419,586],{"class":371},[203,1421,768],{"class":375},[203,1423,771],{"class":582},[203,1425,586],{"class":371},[203,1427,776],{"class":213},[203,1429,592],{"class":375},[203,1431,781],{"class":582},[203,1433,586],{"class":371},[203,1435,776],{"class":213},[203,1437,394],{"class":375},[203,1439,1441],{"class":205,"line":1440},61,[203,1442,401],{"emptyLinePlaceholder":400},[203,1444,1446,1449,1451,1453,1455,1457,1460],{"class":205,"line":1445},62,[203,1447,1448],{"class":375},"    total ",[203,1450,586],{"class":371},[203,1452,648],{"class":375},[203,1454,651],{"class":582},[203,1456,586],{"class":371},[203,1458,1459],{"class":213},"\"total\"",[203,1461,394],{"class":375},[203,1463,1465,1468,1470,1472,1474,1476,1478,1480,1482,1484,1486,1488,1490,1492,1494,1496,1498,1500,1502,1504,1507],{"class":205,"line":1464},63,[203,1466,1467],{"class":375},"    total.font ",[203,1469,586],{"class":371},[203,1471,669],{"class":375},[203,1473,651],{"class":582},[203,1475,586],{"class":371},[203,1477,563],{"class":407},[203,1479,592],{"class":375},[203,1481,680],{"class":582},[203,1483,586],{"class":371},[203,1485,935],{"class":407},[203,1487,592],{"class":375},[203,1489,690],{"class":582},[203,1491,586],{"class":371},[203,1493,695],{"class":407},[203,1495,592],{"class":375},[203,1497,595],{"class":582},[203,1499,586],{"class":371},[203,1501,408],{"class":407},[203,1503,602],{"class":375},[203,1505,1506],{"class":213},"\"ok\"",[203,1508,608],{"class":375},[203,1510,1512,1515,1517,1519,1521,1523,1525,1527,1529,1531,1534],{"class":205,"line":1511},64,[203,1513,1514],{"class":375},"    total.fill ",[203,1516,586],{"class":371},[203,1518,721],{"class":375},[203,1520,724],{"class":213},[203,1522,592],{"class":375},[203,1524,729],{"class":582},[203,1526,586],{"class":371},[203,1528,408],{"class":407},[203,1530,602],{"class":375},[203,1532,1533],{"class":213},"\"ok_soft\"",[203,1535,741],{"class":375},[203,1537,1539,1541,1543,1545,1547,1549],{"class":205,"line":1538},65,[203,1540,747],{"class":582},[203,1542,586],{"class":371},[203,1544,408],{"class":407},[203,1546,602],{"class":375},[203,1548,1533],{"class":213},[203,1550,608],{"class":375},[203,1552,1554,1557,1559],{"class":205,"line":1553},66,[203,1555,1556],{"class":375},"    total.number_format ",[203,1558,586],{"class":371},[203,1560,1212],{"class":213},[203,1562,1564,1567,1569,1571,1574,1576,1578],{"class":205,"line":1563},67,[203,1565,1566],{"class":375},"    total.border ",[203,1568,586],{"class":371},[203,1570,1049],{"class":375},[203,1572,1573],{"class":582},"top",[203,1575,586],{"class":371},[203,1577,574],{"class":407},[203,1579,394],{"class":375},[203,1581,1583,1586,1588,1590,1592,1594,1596,1598,1600,1602,1604],{"class":205,"line":1582},68,[203,1584,1585],{"class":375},"    total.alignment ",[203,1587,586],{"class":371},[203,1589,768],{"class":375},[203,1591,771],{"class":582},[203,1593,586],{"class":371},[203,1595,1229],{"class":213},[203,1597,592],{"class":375},[203,1599,781],{"class":582},[203,1601,586],{"class":371},[203,1603,776],{"class":213},[203,1605,394],{"class":375},[203,1607,1609],{"class":205,"line":1608},69,[203,1610,401],{"emptyLinePlaceholder":400},[203,1612,1614,1617,1619,1621,1623,1625,1628],{"class":205,"line":1613},70,[203,1615,1616],{"class":375},"    warning ",[203,1618,586],{"class":371},[203,1620,648],{"class":375},[203,1622,651],{"class":582},[203,1624,586],{"class":371},[203,1626,1627],{"class":213},"\"warning\"",[203,1629,394],{"class":375},[203,1631,1633,1636,1638,1640,1642,1644,1646,1648,1650,1652,1654,1656,1658,1660,1662,1664,1666,1668,1670,1672,1675],{"class":205,"line":1632},71,[203,1634,1635],{"class":375},"    warning.font ",[203,1637,586],{"class":371},[203,1639,669],{"class":375},[203,1641,651],{"class":582},[203,1643,586],{"class":371},[203,1645,563],{"class":407},[203,1647,592],{"class":375},[203,1649,680],{"class":582},[203,1651,586],{"class":371},[203,1653,935],{"class":407},[203,1655,592],{"class":375},[203,1657,690],{"class":582},[203,1659,586],{"class":371},[203,1661,695],{"class":407},[203,1663,592],{"class":375},[203,1665,595],{"class":582},[203,1667,586],{"class":371},[203,1669,408],{"class":407},[203,1671,602],{"class":375},[203,1673,1674],{"class":213},"\"warn\"",[203,1676,608],{"class":375},[203,1678,1680,1683,1685,1687,1689,1691,1693,1695,1697,1699,1702],{"class":205,"line":1679},72,[203,1681,1682],{"class":375},"    warning.fill ",[203,1684,586],{"class":371},[203,1686,721],{"class":375},[203,1688,724],{"class":213},[203,1690,592],{"class":375},[203,1692,729],{"class":582},[203,1694,586],{"class":371},[203,1696,408],{"class":407},[203,1698,602],{"class":375},[203,1700,1701],{"class":213},"\"warn_soft\"",[203,1703,741],{"class":375},[203,1705,1707,1710,1712,1714,1716,1718],{"class":205,"line":1706},73,[203,1708,1709],{"class":582},"                               end_color",[203,1711,586],{"class":371},[203,1713,408],{"class":407},[203,1715,602],{"class":375},[203,1717,1701],{"class":213},[203,1719,608],{"class":375},[203,1721,1723,1726,1728],{"class":205,"line":1722},74,[203,1724,1725],{"class":375},"    warning.number_format ",[203,1727,586],{"class":371},[203,1729,1212],{"class":213},[203,1731,1733,1736,1738,1740,1742,1744,1746,1748,1750,1752,1754],{"class":205,"line":1732},75,[203,1734,1735],{"class":375},"    warning.alignment ",[203,1737,586],{"class":371},[203,1739,768],{"class":375},[203,1741,771],{"class":582},[203,1743,586],{"class":371},[203,1745,1229],{"class":213},[203,1747,592],{"class":375},[203,1749,781],{"class":582},[203,1751,586],{"class":371},[203,1753,776],{"class":213},[203,1755,394],{"class":375},[203,1757,1759],{"class":205,"line":1758},76,[203,1760,401],{"emptyLinePlaceholder":400},[203,1762,1764,1767],{"class":205,"line":1763},77,[203,1765,1766],{"class":371},"    return",[203,1768,1769],{"class":375}," [title, subtitle, header, body, currency, percent, date,\n",[203,1771,1773],{"class":205,"line":1772},78,[203,1774,1775],{"class":375},"            total, warning]\n",[10,1777,1778,1779,1782,1783,1786],{},"Building them in a ",[225,1780,1781],{},"function"," rather than at module level is the important structural choice. A ",[14,1784,1785],{},"NamedStyle"," binds to the workbook it is added to, so a module-level instance added to a second workbook raises. A factory hands each workbook its own instances.",[189,1788,1790],{"id":1789},"step-2-register-the-theme-on-a-workbook","Step 2 — Register the theme on a workbook",[194,1792,1794],{"className":355,"code":1793,"language":357,"meta":199,"style":199},"# theme.py, continued\ndef apply_theme(wb):\n    \"\"\"Register every theme style on a workbook. Safe to call twice.\"\"\"\n    existing = set(wb.style_names)\n    for style in build_styles():\n        if style.name not in existing:\n            wb.add_named_style(style)\n    return wb\n",[14,1795,1796,1801,1811,1816,1829,1843,1860,1865],{"__ignoreMap":199},[203,1797,1798],{"class":205,"line":206},[203,1799,1800],{"class":364},"# theme.py, continued\n",[203,1802,1803,1805,1808],{"class":205,"line":368},[203,1804,624],{"class":371},[203,1806,1807],{"class":627}," apply_theme",[203,1809,1810],{"class":375},"(wb):\n",[203,1812,1813],{"class":205,"line":385},[203,1814,1815],{"class":213},"    \"\"\"Register every theme style on a workbook. Safe to call twice.\"\"\"\n",[203,1817,1818,1821,1823,1826],{"class":205,"line":391},[203,1819,1820],{"class":375},"    existing ",[203,1822,586],{"class":371},[203,1824,1825],{"class":407}," set",[203,1827,1828],{"class":375},"(wb.style_names)\n",[203,1830,1831,1834,1837,1840],{"class":205,"line":397},[203,1832,1833],{"class":371},"    for",[203,1835,1836],{"class":375}," style ",[203,1838,1839],{"class":371},"in",[203,1841,1842],{"class":375}," build_styles():\n",[203,1844,1845,1848,1851,1854,1857],{"class":205,"line":404},[203,1846,1847],{"class":371},"        if",[203,1849,1850],{"class":375}," style.name ",[203,1852,1853],{"class":371},"not",[203,1855,1856],{"class":371}," in",[203,1858,1859],{"class":375}," existing:\n",[203,1861,1862],{"class":205,"line":417},[203,1863,1864],{"class":375},"            wb.add_named_style(style)\n",[203,1866,1867,1869],{"class":205,"line":432},[203,1868,1766],{"class":371},[203,1870,1871],{"class":375}," wb\n",[10,1873,1874,1875,1878,1879,1882],{},"The ",[14,1876,1877],{},"existing"," check matters because ",[14,1880,1881],{},"add_named_style"," raises on a duplicate name, and a script that opens a template already carrying the theme would otherwise fail on the second run.",[10,1884,1885],{},"Applying a style is then one assignment:",[194,1887,1889],{"className":355,"code":1888,"language":357,"meta":199,"style":199},"from openpyxl import Workbook\nfrom theme import apply_theme\n\nwb = apply_theme(Workbook())\nws = wb.active\n\nws[\"A1\"] = \"Region\"\nws[\"A1\"].style = \"header\"\nws[\"B2\"] = 5150.00\nws[\"B2\"].style = \"currency\"\n\nwb.save(\"report.xlsx\")\n",[14,1890,1891,1903,1915,1919,1929,1939,1943,1959,1973,1987,2000,2004],{"__ignoreMap":199},[203,1892,1893,1895,1898,1900],{"class":205,"line":206},[203,1894,372],{"class":371},[203,1896,1897],{"class":375}," openpyxl ",[203,1899,379],{"class":371},[203,1901,1902],{"class":375}," Workbook\n",[203,1904,1905,1907,1910,1912],{"class":205,"line":368},[203,1906,372],{"class":371},[203,1908,1909],{"class":375}," theme ",[203,1911,379],{"class":371},[203,1913,1914],{"class":375}," apply_theme\n",[203,1916,1917],{"class":205,"line":385},[203,1918,401],{"emptyLinePlaceholder":400},[203,1920,1921,1924,1926],{"class":205,"line":391},[203,1922,1923],{"class":375},"wb ",[203,1925,586],{"class":371},[203,1927,1928],{"class":375}," apply_theme(Workbook())\n",[203,1930,1931,1934,1936],{"class":205,"line":397},[203,1932,1933],{"class":375},"ws ",[203,1935,586],{"class":371},[203,1937,1938],{"class":375}," wb.active\n",[203,1940,1941],{"class":205,"line":404},[203,1942,401],{"emptyLinePlaceholder":400},[203,1944,1945,1948,1951,1954,1956],{"class":205,"line":417},[203,1946,1947],{"class":375},"ws[",[203,1949,1950],{"class":213},"\"A1\"",[203,1952,1953],{"class":375},"] ",[203,1955,586],{"class":371},[203,1957,1958],{"class":213}," \"Region\"\n",[203,1960,1961,1963,1965,1968,1970],{"class":205,"line":432},[203,1962,1947],{"class":375},[203,1964,1950],{"class":213},[203,1966,1967],{"class":375},"].style ",[203,1969,586],{"class":371},[203,1971,1972],{"class":213}," \"header\"\n",[203,1974,1975,1977,1980,1982,1984],{"class":205,"line":445},[203,1976,1947],{"class":375},[203,1978,1979],{"class":213},"\"B2\"",[203,1981,1953],{"class":375},[203,1983,586],{"class":371},[203,1985,1986],{"class":407}," 5150.00\n",[203,1988,1989,1991,1993,1995,1997],{"class":205,"line":458},[203,1990,1947],{"class":375},[203,1992,1979],{"class":213},[203,1994,1967],{"class":375},[203,1996,586],{"class":371},[203,1998,1999],{"class":213}," \"currency\"\n",[203,2001,2002],{"class":205,"line":471},[203,2003,401],{"emptyLinePlaceholder":400},[203,2005,2006,2009,2012],{"class":205,"line":484},[203,2007,2008],{"class":375},"wb.save(",[203,2010,2011],{"class":213},"\"report.xlsx\"",[203,2013,394],{"class":375},[189,2015,2017],{"id":2016},"step-3-apply-styles-by-column-role","Step 3 — Apply styles by column role",[10,2019,2020],{},"Assigning cell by cell is where scripts get repetitive. Drive it from a column-to-role mapping instead:",[194,2022,2024],{"className":355,"code":2023,"language":357,"meta":199,"style":199},"from openpyxl.utils import get_column_letter\n\nCOLUMN_ROLES = {\n    \"region\": \"body\",\n    \"owner\": \"body\",\n    \"invoice_date\": \"date\",\n    \"revenue\": \"currency\",\n    \"margin\": \"percent\",\n}\n\ndef style_table(ws, header_row=1, roles=COLUMN_ROLES, default=\"body\"):\n    \"\"\"Apply the theme to a table using a column-name to role mapping.\"\"\"\n    headers = {}\n    for cell in ws[header_row]:\n        if cell.value is None:\n            continue\n        cell.style = \"header\"\n        headers[str(cell.value).strip()] = cell.column\n\n    for name, column in headers.items():\n        role = roles.get(name, default)\n        for (cell,) in ws.iter_rows(min_row=header_row + 1,\n                                    min_col=column, max_col=column):\n            if cell.value is not None:\n                cell.style = role\n\n    ws.freeze_panes = ws.cell(row=header_row + 1, column=1).coordinate\n    return headers\n",[14,2025,2026,2038,2042,2051,2062,2073,2084,2095,2106,2110,2114,2146,2151,2161,2173,2189,2194,2203,2219,2223,2235,2245,2274,2292,2308,2318,2322,2355],{"__ignoreMap":199},[203,2027,2028,2030,2033,2035],{"class":205,"line":206},[203,2029,372],{"class":371},[203,2031,2032],{"class":375}," openpyxl.utils ",[203,2034,379],{"class":371},[203,2036,2037],{"class":375}," get_column_letter\n",[203,2039,2040],{"class":205,"line":368},[203,2041,401],{"emptyLinePlaceholder":400},[203,2043,2044,2047,2049],{"class":205,"line":385},[203,2045,2046],{"class":407},"COLUMN_ROLES",[203,2048,411],{"class":371},[203,2050,414],{"class":375},[203,2052,2053,2056,2058,2060],{"class":205,"line":391},[203,2054,2055],{"class":213},"    \"region\"",[203,2057,423],{"class":375},[203,2059,1080],{"class":213},[203,2061,429],{"class":375},[203,2063,2064,2067,2069,2071],{"class":205,"line":397},[203,2065,2066],{"class":213},"    \"owner\"",[203,2068,423],{"class":375},[203,2070,1080],{"class":213},[203,2072,429],{"class":375},[203,2074,2075,2078,2080,2082],{"class":205,"line":404},[203,2076,2077],{"class":213},"    \"invoice_date\"",[203,2079,423],{"class":375},[203,2081,1360],{"class":213},[203,2083,429],{"class":375},[203,2085,2086,2089,2091,2093],{"class":205,"line":417},[203,2087,2088],{"class":213},"    \"revenue\"",[203,2090,423],{"class":375},[203,2092,1161],{"class":213},[203,2094,429],{"class":375},[203,2096,2097,2100,2102,2104],{"class":205,"line":432},[203,2098,2099],{"class":213},"    \"margin\"",[203,2101,423],{"class":375},[203,2103,1261],{"class":213},[203,2105,429],{"class":375},[203,2107,2108],{"class":205,"line":445},[203,2109,552],{"class":375},[203,2111,2112],{"class":205,"line":458},[203,2113,401],{"emptyLinePlaceholder":400},[203,2115,2116,2118,2121,2124,2126,2129,2132,2134,2136,2139,2141,2143],{"class":205,"line":471},[203,2117,624],{"class":371},[203,2119,2120],{"class":627}," style_table",[203,2122,2123],{"class":375},"(ws, header_row",[203,2125,586],{"class":371},[203,2127,2128],{"class":407},"1",[203,2130,2131],{"class":375},", roles",[203,2133,586],{"class":371},[203,2135,2046],{"class":407},[203,2137,2138],{"class":375},", default",[203,2140,586],{"class":371},[203,2142,1080],{"class":213},[203,2144,2145],{"class":375},"):\n",[203,2147,2148],{"class":205,"line":484},[203,2149,2150],{"class":213},"    \"\"\"Apply the theme to a table using a column-name to role mapping.\"\"\"\n",[203,2152,2153,2156,2158],{"class":205,"line":497},[203,2154,2155],{"class":375},"    headers ",[203,2157,586],{"class":371},[203,2159,2160],{"class":375}," {}\n",[203,2162,2163,2165,2168,2170],{"class":205,"line":510},[203,2164,1833],{"class":371},[203,2166,2167],{"class":375}," cell ",[203,2169,1839],{"class":371},[203,2171,2172],{"class":375}," ws[header_row]:\n",[203,2174,2175,2177,2180,2183,2186],{"class":205,"line":523},[203,2176,1847],{"class":371},[203,2178,2179],{"class":375}," cell.value ",[203,2181,2182],{"class":371},"is",[203,2184,2185],{"class":407}," None",[203,2187,2188],{"class":375},":\n",[203,2190,2191],{"class":205,"line":536},[203,2192,2193],{"class":371},"            continue\n",[203,2195,2196,2199,2201],{"class":205,"line":549},[203,2197,2198],{"class":375},"        cell.style ",[203,2200,586],{"class":371},[203,2202,1972],{"class":213},[203,2204,2205,2208,2211,2214,2216],{"class":205,"line":555},[203,2206,2207],{"class":375},"        headers[",[203,2209,2210],{"class":407},"str",[203,2212,2213],{"class":375},"(cell.value).strip()] ",[203,2215,586],{"class":371},[203,2217,2218],{"class":375}," cell.column\n",[203,2220,2221],{"class":205,"line":560},[203,2222,401],{"emptyLinePlaceholder":400},[203,2224,2225,2227,2230,2232],{"class":205,"line":571},[203,2226,1833],{"class":371},[203,2228,2229],{"class":375}," name, column ",[203,2231,1839],{"class":371},[203,2233,2234],{"class":375}," headers.items():\n",[203,2236,2237,2240,2242],{"class":205,"line":611},[203,2238,2239],{"class":375},"        role ",[203,2241,586],{"class":371},[203,2243,2244],{"class":375}," roles.get(name, default)\n",[203,2246,2247,2250,2253,2255,2258,2261,2263,2266,2269,2272],{"class":205,"line":616},[203,2248,2249],{"class":371},"        for",[203,2251,2252],{"class":375}," (cell,) ",[203,2254,1839],{"class":371},[203,2256,2257],{"class":375}," ws.iter_rows(",[203,2259,2260],{"class":582},"min_row",[203,2262,586],{"class":371},[203,2264,2265],{"class":375},"header_row ",[203,2267,2268],{"class":371},"+",[203,2270,2271],{"class":407}," 1",[203,2273,429],{"class":375},[203,2275,2276,2279,2281,2284,2287,2289],{"class":205,"line":621},[203,2277,2278],{"class":582},"                                    min_col",[203,2280,586],{"class":371},[203,2282,2283],{"class":375},"column, ",[203,2285,2286],{"class":582},"max_col",[203,2288,586],{"class":371},[203,2290,2291],{"class":375},"column):\n",[203,2293,2294,2297,2299,2301,2304,2306],{"class":205,"line":634},[203,2295,2296],{"class":371},"            if",[203,2298,2179],{"class":375},[203,2300,2182],{"class":371},[203,2302,2303],{"class":371}," not",[203,2305,2185],{"class":407},[203,2307,2188],{"class":375},[203,2309,2310,2313,2315],{"class":205,"line":640},[203,2311,2312],{"class":375},"                cell.style ",[203,2314,586],{"class":371},[203,2316,2317],{"class":375}," role\n",[203,2319,2320],{"class":205,"line":661},[203,2321,401],{"emptyLinePlaceholder":400},[203,2323,2324,2327,2329,2332,2335,2337,2339,2341,2343,2345,2348,2350,2352],{"class":205,"line":713},[203,2325,2326],{"class":375},"    ws.freeze_panes ",[203,2328,586],{"class":371},[203,2330,2331],{"class":375}," ws.cell(",[203,2333,2334],{"class":582},"row",[203,2336,586],{"class":371},[203,2338,2265],{"class":375},[203,2340,2268],{"class":371},[203,2342,2271],{"class":407},[203,2344,592],{"class":375},[203,2346,2347],{"class":582},"column",[203,2349,586],{"class":371},[203,2351,2128],{"class":407},[203,2353,2354],{"class":375},").coordinate\n",[203,2356,2357,2359],{"class":205,"line":744},[203,2358,1766],{"class":371},[203,2360,2361],{"class":375}," headers\n",[10,2363,2364,2365,23],{},"Keying on the header text rather than the column position means an inserted column does not misformat everything to its right — the same reasoning as keying data rows by header in ",[19,2366,2368],{"href":2367},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fiterate-over-rows-and-columns-with-openpyxl\u002F","iterating rows and columns with openpyxl",[10,2370,2371],{},"A total row and conditional warnings slot in the same way:",[194,2373,2375],{"className":355,"code":2374,"language":357,"meta":199,"style":199},"def style_total_row(ws, row, first_col=1, last_col=None):\n    last_col = last_col or ws.max_column\n    for col in range(first_col, last_col + 1):\n        ws.cell(row=row, column=col).style = \"total\"\n\ndef flag_above(ws, column_letter, threshold, header_row=1):\n    \"\"\"Re-style cells above a threshold with the warning role.\"\"\"\n    from openpyxl.utils import column_index_from_string\n    col = column_index_from_string(column_letter)\n    flagged = 0\n    for (cell,) in ws.iter_rows(min_row=header_row + 1, min_col=col, max_col=col):\n        if isinstance(cell.value, (int, float)) and cell.value > threshold:\n            cell.style = \"warning\"\n            flagged += 1\n    return flagged\n",[14,2376,2377,2401,2417,2438,2462,2466,2482,2487,2499,2509,2519,2556,2588,2598,2609],{"__ignoreMap":199},[203,2378,2379,2381,2384,2387,2389,2391,2394,2396,2399],{"class":205,"line":206},[203,2380,624],{"class":371},[203,2382,2383],{"class":627}," style_total_row",[203,2385,2386],{"class":375},"(ws, row, first_col",[203,2388,586],{"class":371},[203,2390,2128],{"class":407},[203,2392,2393],{"class":375},", last_col",[203,2395,586],{"class":371},[203,2397,2398],{"class":407},"None",[203,2400,2145],{"class":375},[203,2402,2403,2406,2408,2411,2414],{"class":205,"line":368},[203,2404,2405],{"class":375},"    last_col ",[203,2407,586],{"class":371},[203,2409,2410],{"class":375}," last_col ",[203,2412,2413],{"class":371},"or",[203,2415,2416],{"class":375}," ws.max_column\n",[203,2418,2419,2421,2424,2426,2429,2432,2434,2436],{"class":205,"line":385},[203,2420,1833],{"class":371},[203,2422,2423],{"class":375}," col ",[203,2425,1839],{"class":371},[203,2427,2428],{"class":407}," range",[203,2430,2431],{"class":375},"(first_col, last_col ",[203,2433,2268],{"class":371},[203,2435,2271],{"class":407},[203,2437,2145],{"class":375},[203,2439,2440,2443,2445,2447,2450,2452,2454,2457,2459],{"class":205,"line":391},[203,2441,2442],{"class":375},"        ws.cell(",[203,2444,2334],{"class":582},[203,2446,586],{"class":371},[203,2448,2449],{"class":375},"row, ",[203,2451,2347],{"class":582},[203,2453,586],{"class":371},[203,2455,2456],{"class":375},"col).style ",[203,2458,586],{"class":371},[203,2460,2461],{"class":213}," \"total\"\n",[203,2463,2464],{"class":205,"line":397},[203,2465,401],{"emptyLinePlaceholder":400},[203,2467,2468,2470,2473,2476,2478,2480],{"class":205,"line":404},[203,2469,624],{"class":371},[203,2471,2472],{"class":627}," flag_above",[203,2474,2475],{"class":375},"(ws, column_letter, threshold, header_row",[203,2477,586],{"class":371},[203,2479,2128],{"class":407},[203,2481,2145],{"class":375},[203,2483,2484],{"class":205,"line":417},[203,2485,2486],{"class":213},"    \"\"\"Re-style cells above a threshold with the warning role.\"\"\"\n",[203,2488,2489,2492,2494,2496],{"class":205,"line":432},[203,2490,2491],{"class":371},"    from",[203,2493,2032],{"class":375},[203,2495,379],{"class":371},[203,2497,2498],{"class":375}," column_index_from_string\n",[203,2500,2501,2504,2506],{"class":205,"line":445},[203,2502,2503],{"class":375},"    col ",[203,2505,586],{"class":371},[203,2507,2508],{"class":375}," column_index_from_string(column_letter)\n",[203,2510,2511,2514,2516],{"class":205,"line":458},[203,2512,2513],{"class":375},"    flagged ",[203,2515,586],{"class":371},[203,2517,2518],{"class":407}," 0\n",[203,2520,2521,2523,2525,2527,2529,2531,2533,2535,2537,2539,2541,2544,2546,2549,2551,2553],{"class":205,"line":471},[203,2522,1833],{"class":371},[203,2524,2252],{"class":375},[203,2526,1839],{"class":371},[203,2528,2257],{"class":375},[203,2530,2260],{"class":582},[203,2532,586],{"class":371},[203,2534,2265],{"class":375},[203,2536,2268],{"class":371},[203,2538,2271],{"class":407},[203,2540,592],{"class":375},[203,2542,2543],{"class":582},"min_col",[203,2545,586],{"class":371},[203,2547,2548],{"class":375},"col, ",[203,2550,2286],{"class":582},[203,2552,586],{"class":371},[203,2554,2555],{"class":375},"col):\n",[203,2557,2558,2560,2563,2566,2569,2571,2574,2577,2580,2582,2585],{"class":205,"line":484},[203,2559,1847],{"class":371},[203,2561,2562],{"class":407}," isinstance",[203,2564,2565],{"class":375},"(cell.value, (",[203,2567,2568],{"class":407},"int",[203,2570,592],{"class":375},[203,2572,2573],{"class":407},"float",[203,2575,2576],{"class":375},")) ",[203,2578,2579],{"class":371},"and",[203,2581,2179],{"class":375},[203,2583,2584],{"class":371},">",[203,2586,2587],{"class":375}," threshold:\n",[203,2589,2590,2593,2595],{"class":205,"line":497},[203,2591,2592],{"class":375},"            cell.style ",[203,2594,586],{"class":371},[203,2596,2597],{"class":213}," \"warning\"\n",[203,2599,2600,2603,2606],{"class":205,"line":510},[203,2601,2602],{"class":375},"            flagged ",[203,2604,2605],{"class":371},"+=",[203,2607,2608],{"class":407}," 1\n",[203,2610,2611,2613],{"class":205,"line":523},[203,2612,1766],{"class":371},[203,2614,2615],{"class":375}," flagged\n",[10,2617,2618,2619,2622,2623,2626,2627,2629,2630,2632,2633,2635],{},"Note that re-assigning ",[14,2620,2621],{},"cell.style"," ",[225,2624,2625],{},"replaces"," the previous style entirely rather than merging — a cell styled ",[14,2628,95],{}," and then ",[14,2631,115],{}," keeps only the warning's number format, which is why ",[14,2634,115],{}," defines one. Styles are whole roles, not modifiers.",[189,2637,2639],{"id":2638},"step-4-avoid-the-format-ceiling","Step 4 — Avoid the format ceiling",[10,2641,2642],{},"Excel caps a workbook at roughly 64,000 distinct cell formats. A script that constructs style objects inside a loop reaches it on a large sheet, and the file then fails to open with a \"too many different cell formats\" error.",[25,2644,35,2650,35,2653,35,2656,35,2659,35,2664,35,2669,35,2675,35,2679,35,2684,35,2690,35,2696,35,2699,35,2703,35,2706,35,2711],{"viewBox":2645,"role":28,"ariaLabel":2646,"ariaLabelledBy":2647,"xmlns":33,"style":34},"0 0 800 236","Style entries created per approach: a new Font per cell creates one entry per cell and approaches Excel's ceiling, while a NamedStyle creates one entry no matter how many cells reference it.",[2648,2649],"ceil-t","ceil-d",[37,2651,2652],{"id":2648},"Where the 64,000-format ceiling comes from",[41,2654,2655],{"id":2649},"Two approaches to styling one hundred thousand cells. Constructing a Font or PatternFill inside the loop registers a distinct format entry per cell, so the count climbs towards and past Excel's ceiling of roughly sixty-four thousand and the workbook becomes unopenable. Assigning a NamedStyle by name registers exactly one entry regardless of how many cells reference it, so the count stays at the number of roles in the theme.",[45,2657],{"x":47,"y":47,"width":48,"height":2658,"fill":50},"236",[61,2660,2663],{"x":2661,"y":72,"style":2662},"400","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","styling 100,000 cells",[45,2665],{"x":53,"y":2666,"width":2667,"height":2668,"rx":73,"fill":57,"stroke":58},"48","180","30",[61,2670,2674],{"x":2671,"y":2672,"style":2673},"104","68","font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Font() in the loop",[45,2676],{"x":108,"y":2666,"width":2677,"height":2668,"rx":2678,"fill":109,"stroke":110,"style":59},"530","5",[61,2680,2683],{"x":2681,"y":2672,"style":2682},"475","font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","100,000 distinct format entries",[205,2685],{"x1":2686,"y1":2687,"x2":2686,"y2":2688,"stroke":110,"style":2689},"550","42","118","stroke-width:2.5px;stroke-dasharray:5 4",[61,2691,2695],{"x":2692,"y":2693,"style":2694},"556","112","font-size:10px;font-weight:700;fill:var(--accent-ink,#be185d)","Excel's ~64,000 ceiling",[45,2697],{"x":53,"y":2698,"width":2667,"height":2668,"rx":73,"fill":57,"stroke":58},"130",[61,2700,2702],{"x":2671,"y":2701,"style":2673},"150","cell.style = \"currency\"",[45,2704],{"x":108,"y":2698,"width":2705,"height":2668,"rx":2678,"fill":99,"stroke":100,"style":59},"16",[61,2707,2710],{"x":2708,"y":2701,"style":2709},"238","font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157)","9 entries — one per role in the theme",[61,2712,2715],{"x":2661,"y":2713,"style":2714},"200","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","the count depends on how many distinct styles exist, not on how many cells use them",[194,2717,2719],{"className":355,"code":2718,"language":357,"meta":199,"style":199},"from openpyxl.styles import Font, PatternFill\n\n# Wrong: a new Font and PatternFill object per cell.\nfor row in ws.iter_rows(min_row=2, max_row=100_000):\n    for cell in row:\n        cell.font = Font(name=\"Calibri\", size=11, color=\"172033\")\n        cell.fill = PatternFill(\"solid\", start_color=\"FFFFFF\",\n                                end_color=\"FFFFFF\")\n\n# Right: one named style, referenced by name.\nfor row in ws.iter_rows(min_row=2, max_row=100_000):\n    for cell in row:\n        cell.style = \"body\"\n",[14,2720,2721,2732,2736,2741,2772,2783,2817,2838,2849,2853,2858,2884,2894],{"__ignoreMap":199},[203,2722,2723,2725,2727,2729],{"class":205,"line":206},[203,2724,372],{"class":371},[203,2726,376],{"class":375},[203,2728,379],{"class":371},[203,2730,2731],{"class":375}," Font, PatternFill\n",[203,2733,2734],{"class":205,"line":368},[203,2735,401],{"emptyLinePlaceholder":400},[203,2737,2738],{"class":205,"line":385},[203,2739,2740],{"class":364},"# Wrong: a new Font and PatternFill object per cell.\n",[203,2742,2743,2746,2749,2751,2753,2755,2757,2760,2762,2765,2767,2770],{"class":205,"line":391},[203,2744,2745],{"class":371},"for",[203,2747,2748],{"class":375}," row ",[203,2750,1839],{"class":371},[203,2752,2257],{"class":375},[203,2754,2260],{"class":582},[203,2756,586],{"class":371},[203,2758,2759],{"class":407},"2",[203,2761,592],{"class":375},[203,2763,2764],{"class":582},"max_row",[203,2766,586],{"class":371},[203,2768,2769],{"class":407},"100_000",[203,2771,2145],{"class":375},[203,2773,2774,2776,2778,2780],{"class":205,"line":397},[203,2775,1833],{"class":371},[203,2777,2167],{"class":375},[203,2779,1839],{"class":371},[203,2781,2782],{"class":375}," row:\n",[203,2784,2785,2788,2790,2792,2794,2796,2799,2801,2803,2805,2807,2809,2811,2813,2815],{"class":205,"line":404},[203,2786,2787],{"class":375},"        cell.font ",[203,2789,586],{"class":371},[203,2791,669],{"class":375},[203,2793,651],{"class":582},[203,2795,586],{"class":371},[203,2797,2798],{"class":213},"\"Calibri\"",[203,2800,592],{"class":375},[203,2802,680],{"class":582},[203,2804,586],{"class":371},[203,2806,935],{"class":407},[203,2808,592],{"class":375},[203,2810,595],{"class":582},[203,2812,586],{"class":371},[203,2814,453],{"class":213},[203,2816,394],{"class":375},[203,2818,2819,2822,2824,2826,2828,2830,2832,2834,2836],{"class":205,"line":417},[203,2820,2821],{"class":375},"        cell.fill ",[203,2823,586],{"class":371},[203,2825,721],{"class":375},[203,2827,724],{"class":213},[203,2829,592],{"class":375},[203,2831,729],{"class":582},[203,2833,586],{"class":371},[203,2835,544],{"class":213},[203,2837,429],{"class":375},[203,2839,2840,2843,2845,2847],{"class":205,"line":432},[203,2841,2842],{"class":582},"                                end_color",[203,2844,586],{"class":371},[203,2846,544],{"class":213},[203,2848,394],{"class":375},[203,2850,2851],{"class":205,"line":445},[203,2852,401],{"emptyLinePlaceholder":400},[203,2854,2855],{"class":205,"line":458},[203,2856,2857],{"class":364},"# Right: one named style, referenced by name.\n",[203,2859,2860,2862,2864,2866,2868,2870,2872,2874,2876,2878,2880,2882],{"class":205,"line":471},[203,2861,2745],{"class":371},[203,2863,2748],{"class":375},[203,2865,1839],{"class":371},[203,2867,2257],{"class":375},[203,2869,2260],{"class":582},[203,2871,586],{"class":371},[203,2873,2759],{"class":407},[203,2875,592],{"class":375},[203,2877,2764],{"class":582},[203,2879,586],{"class":371},[203,2881,2769],{"class":407},[203,2883,2145],{"class":375},[203,2885,2886,2888,2890,2892],{"class":205,"line":484},[203,2887,1833],{"class":371},[203,2889,2167],{"class":375},[203,2891,1839],{"class":371},[203,2893,2782],{"class":375},[203,2895,2896,2898,2900],{"class":205,"line":497},[203,2897,2198],{"class":375},[203,2899,586],{"class":371},[203,2901,2902],{"class":213}," \"body\"\n",[10,2904,2905],{},"openpyxl does deduplicate identical style objects on save, so the wrong version above may survive — but it allocates a hundred thousand short-lived objects to get there, and any variation between iterations (a colour derived from the value, say) defeats the deduplication entirely and hits the ceiling for real.",[189,2907,2909],{"id":2908},"step-5-share-the-theme-across-reports","Step 5 — Share the theme across reports",[25,2911,35,2917,35,2920,35,2923,35,2926,35,2929,35,2934,35,2940,35,2945,35,2949,35,2953,35,2958,35,2961,35,2966,35,2969,35,2972,35,2975],{"viewBox":2912,"role":28,"ariaLabel":2913,"ariaLabelledBy":2914,"xmlns":33,"style":34},"0 0 800 226","Roles named by meaning survive a redesign, while roles named by appearance become misleading the moment the palette changes.",[2915,2916],"roles-t","roles-d",[37,2918,2919],{"id":2915},"Name styles by what they mean, not by how they look",[41,2921,2922],{"id":2916},"Two naming schemes after a rebrand changes the accent colour from indigo to teal. Roles named by meaning — header, total, warning — still describe what they are for, and only the palette changed. Roles named by appearance — indigo_bold, green_fill — now lie about their own colour, so every call site has to be found and renamed or the code becomes actively misleading.",[45,2924],{"x":47,"y":47,"width":48,"height":2925,"fill":50},"226",[61,2927,2928],{"x":2661,"y":72,"style":2662},"after a rebrand changes the accent colour",[45,2930],{"x":53,"y":2931,"width":2932,"height":2933,"rx":53,"fill":99,"stroke":100,"style":59},"44","368","160",[61,2935,2939],{"x":2936,"y":2937,"style":2938},"198","72","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","named by meaning",[61,2941,2944],{"x":2936,"y":2942,"style":2943},"102","font-size:11.5px;fill:var(--text,#172033);text-anchor:middle","header · total · warning · currency",[61,2946,2948],{"x":2936,"y":2947,"style":2714},"134","still accurate; only the palette",[61,2950,2952],{"x":2936,"y":2951,"style":2714},"154","definition changed",[61,2954,2957],{"x":2936,"y":2955,"style":2956},"184","font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","one edit, no call sites touched",[45,2959],{"x":2960,"y":2931,"width":2932,"height":2933,"rx":53,"fill":109,"stroke":110,"style":59},"418",[61,2962,2965],{"x":2963,"y":2937,"style":2964},"602","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","named by appearance",[61,2967,2968],{"x":2963,"y":2942,"style":2943},"indigo_bold · green_fill · pink_cell",[61,2970,2971],{"x":2963,"y":2947,"style":2714},"now lying about their own colour",[61,2973,2974],{"x":2963,"y":2951,"style":2714},"or frozen at the old palette",[61,2976,2977],{"x":2963,"y":2955,"style":2682},"every call site must be renamed",[10,2979,2980,2981,2984],{},"Because ",[14,2982,2983],{},"build_styles"," is a function, every workbook gets its own instances from the same definitions:",[194,2986,2988],{"className":355,"code":2987,"language":357,"meta":199,"style":199},"from openpyxl import Workbook, load_workbook\nfrom theme import apply_theme, style_table, style_total_row\n\ndef new_report(title, subtitle, columns):\n    \"\"\"A blank workbook with the theme applied and a banner in place.\"\"\"\n    wb = apply_theme(Workbook())\n    ws = wb.active\n\n    ws[\"A1\"] = title\n    ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=columns)\n    ws[\"A1\"].style = \"title\"\n    ws.row_dimensions[1].height = 30\n\n    ws[\"A2\"] = subtitle\n    ws.merge_cells(start_row=2, start_column=1, end_row=2, end_column=columns)\n    ws[\"A2\"].style = \"subtitle\"\n\n    return wb, ws\n\n# Every report in the project starts the same way.\nwb, ws = new_report(\"Regional Revenue\", \"August 2026\", columns=5)\n",[14,2989,2990,3001,3012,3016,3026,3031,3040,3049,3053,3067,3107,3120,3135,3139,3153,3187,3200,3204,3211,3215,3220],{"__ignoreMap":199},[203,2991,2992,2994,2996,2998],{"class":205,"line":206},[203,2993,372],{"class":371},[203,2995,1897],{"class":375},[203,2997,379],{"class":371},[203,2999,3000],{"class":375}," Workbook, load_workbook\n",[203,3002,3003,3005,3007,3009],{"class":205,"line":368},[203,3004,372],{"class":371},[203,3006,1909],{"class":375},[203,3008,379],{"class":371},[203,3010,3011],{"class":375}," apply_theme, style_table, style_total_row\n",[203,3013,3014],{"class":205,"line":385},[203,3015,401],{"emptyLinePlaceholder":400},[203,3017,3018,3020,3023],{"class":205,"line":391},[203,3019,624],{"class":371},[203,3021,3022],{"class":627}," new_report",[203,3024,3025],{"class":375},"(title, subtitle, columns):\n",[203,3027,3028],{"class":205,"line":397},[203,3029,3030],{"class":213},"    \"\"\"A blank workbook with the theme applied and a banner in place.\"\"\"\n",[203,3032,3033,3036,3038],{"class":205,"line":404},[203,3034,3035],{"class":375},"    wb ",[203,3037,586],{"class":371},[203,3039,1928],{"class":375},[203,3041,3042,3045,3047],{"class":205,"line":417},[203,3043,3044],{"class":375},"    ws ",[203,3046,586],{"class":371},[203,3048,1938],{"class":375},[203,3050,3051],{"class":205,"line":432},[203,3052,401],{"emptyLinePlaceholder":400},[203,3054,3055,3058,3060,3062,3064],{"class":205,"line":445},[203,3056,3057],{"class":375},"    ws[",[203,3059,1950],{"class":213},[203,3061,1953],{"class":375},[203,3063,586],{"class":371},[203,3065,3066],{"class":375}," title\n",[203,3068,3069,3072,3075,3077,3079,3081,3084,3086,3088,3090,3093,3095,3097,3099,3102,3104],{"class":205,"line":458},[203,3070,3071],{"class":375},"    ws.merge_cells(",[203,3073,3074],{"class":582},"start_row",[203,3076,586],{"class":371},[203,3078,2128],{"class":407},[203,3080,592],{"class":375},[203,3082,3083],{"class":582},"start_column",[203,3085,586],{"class":371},[203,3087,2128],{"class":407},[203,3089,592],{"class":375},[203,3091,3092],{"class":582},"end_row",[203,3094,586],{"class":371},[203,3096,2128],{"class":407},[203,3098,592],{"class":375},[203,3100,3101],{"class":582},"end_column",[203,3103,586],{"class":371},[203,3105,3106],{"class":375},"columns)\n",[203,3108,3109,3111,3113,3115,3117],{"class":205,"line":471},[203,3110,3057],{"class":375},[203,3112,1950],{"class":213},[203,3114,1967],{"class":375},[203,3116,586],{"class":371},[203,3118,3119],{"class":213}," \"title\"\n",[203,3121,3122,3125,3127,3130,3132],{"class":205,"line":484},[203,3123,3124],{"class":375},"    ws.row_dimensions[",[203,3126,2128],{"class":407},[203,3128,3129],{"class":375},"].height ",[203,3131,586],{"class":371},[203,3133,3134],{"class":407}," 30\n",[203,3136,3137],{"class":205,"line":497},[203,3138,401],{"emptyLinePlaceholder":400},[203,3140,3141,3143,3146,3148,3150],{"class":205,"line":510},[203,3142,3057],{"class":375},[203,3144,3145],{"class":213},"\"A2\"",[203,3147,1953],{"class":375},[203,3149,586],{"class":371},[203,3151,3152],{"class":375}," subtitle\n",[203,3154,3155,3157,3159,3161,3163,3165,3167,3169,3171,3173,3175,3177,3179,3181,3183,3185],{"class":205,"line":523},[203,3156,3071],{"class":375},[203,3158,3074],{"class":582},[203,3160,586],{"class":371},[203,3162,2759],{"class":407},[203,3164,592],{"class":375},[203,3166,3083],{"class":582},[203,3168,586],{"class":371},[203,3170,2128],{"class":407},[203,3172,592],{"class":375},[203,3174,3092],{"class":582},[203,3176,586],{"class":371},[203,3178,2759],{"class":407},[203,3180,592],{"class":375},[203,3182,3101],{"class":582},[203,3184,586],{"class":371},[203,3186,3106],{"class":375},[203,3188,3189,3191,3193,3195,3197],{"class":205,"line":536},[203,3190,3057],{"class":375},[203,3192,3145],{"class":213},[203,3194,1967],{"class":375},[203,3196,586],{"class":371},[203,3198,3199],{"class":213}," \"subtitle\"\n",[203,3201,3202],{"class":205,"line":549},[203,3203,401],{"emptyLinePlaceholder":400},[203,3205,3206,3208],{"class":205,"line":555},[203,3207,1766],{"class":371},[203,3209,3210],{"class":375}," wb, ws\n",[203,3212,3213],{"class":205,"line":560},[203,3214,401],{"emptyLinePlaceholder":400},[203,3216,3217],{"class":205,"line":571},[203,3218,3219],{"class":364},"# Every report in the project starts the same way.\n",[203,3221,3222,3225,3227,3230,3233,3235,3238,3240,3243,3245,3247],{"class":205,"line":611},[203,3223,3224],{"class":375},"wb, ws ",[203,3226,586],{"class":371},[203,3228,3229],{"class":375}," new_report(",[203,3231,3232],{"class":213},"\"Regional Revenue\"",[203,3234,592],{"class":375},[203,3236,3237],{"class":213},"\"August 2026\"",[203,3239,592],{"class":375},[203,3241,3242],{"class":582},"columns",[203,3244,586],{"class":371},[203,3246,2678],{"class":407},[203,3248,394],{"class":375},[10,3250,3251],{},"For a report produced through pandas, apply the theme in a second pass after writing:",[194,3253,3255],{"className":355,"code":3254,"language":357,"meta":199,"style":199},"import pandas as pd\nfrom openpyxl import load_workbook\nfrom theme import apply_theme, style_table\n\ndef write_themed(df, path, sheet_name=\"Report\"):\n    df.to_excel(path, sheet_name=sheet_name, index=False, engine=\"openpyxl\")\n\n    wb = apply_theme(load_workbook(path))\n    style_table(wb[sheet_name])\n    wb.save(path)\n    return path\n",[14,3256,3257,3270,3281,3292,3296,3313,3346,3350,3359,3364,3369],{"__ignoreMap":199},[203,3258,3259,3261,3264,3267],{"class":205,"line":206},[203,3260,379],{"class":371},[203,3262,3263],{"class":375}," pandas ",[203,3265,3266],{"class":371},"as",[203,3268,3269],{"class":375}," pd\n",[203,3271,3272,3274,3276,3278],{"class":205,"line":368},[203,3273,372],{"class":371},[203,3275,1897],{"class":375},[203,3277,379],{"class":371},[203,3279,3280],{"class":375}," load_workbook\n",[203,3282,3283,3285,3287,3289],{"class":205,"line":385},[203,3284,372],{"class":371},[203,3286,1909],{"class":375},[203,3288,379],{"class":371},[203,3290,3291],{"class":375}," apply_theme, style_table\n",[203,3293,3294],{"class":205,"line":391},[203,3295,401],{"emptyLinePlaceholder":400},[203,3297,3298,3300,3303,3306,3308,3311],{"class":205,"line":397},[203,3299,624],{"class":371},[203,3301,3302],{"class":627}," write_themed",[203,3304,3305],{"class":375},"(df, path, sheet_name",[203,3307,586],{"class":371},[203,3309,3310],{"class":213},"\"Report\"",[203,3312,2145],{"class":375},[203,3314,3315,3318,3321,3323,3326,3329,3331,3334,3336,3339,3341,3344],{"class":205,"line":404},[203,3316,3317],{"class":375},"    df.to_excel(path, ",[203,3319,3320],{"class":582},"sheet_name",[203,3322,586],{"class":371},[203,3324,3325],{"class":375},"sheet_name, ",[203,3327,3328],{"class":582},"index",[203,3330,586],{"class":371},[203,3332,3333],{"class":407},"False",[203,3335,592],{"class":375},[203,3337,3338],{"class":582},"engine",[203,3340,586],{"class":371},[203,3342,3343],{"class":213},"\"openpyxl\"",[203,3345,394],{"class":375},[203,3347,3348],{"class":205,"line":417},[203,3349,401],{"emptyLinePlaceholder":400},[203,3351,3352,3354,3356],{"class":205,"line":432},[203,3353,3035],{"class":375},[203,3355,586],{"class":371},[203,3357,3358],{"class":375}," apply_theme(load_workbook(path))\n",[203,3360,3361],{"class":205,"line":445},[203,3362,3363],{"class":375},"    style_table(wb[sheet_name])\n",[203,3365,3366],{"class":205,"line":458},[203,3367,3368],{"class":375},"    wb.save(path)\n",[203,3370,3371,3373],{"class":205,"line":471},[203,3372,1766],{"class":371},[203,3374,3375],{"class":375}," path\n",[10,3377,3378,3379,3382,3383,23],{},"The two-pass shape is unavoidable with openpyxl, because ",[14,3380,3381],{},"to_excel"," replaces the sheet and would discard styling applied first. The banner mechanics are in ",[19,3384,3386],{"href":3385},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fmerge-cells-and-centre-a-report-title-with-openpyxl\u002F","merging cells and centring a report title",[189,3388,3390],{"id":3389},"common-pitfalls-and-fixes","Common pitfalls and fixes",[243,3392,3393,3406],{},[246,3394,3395],{},[249,3396,3397,3400,3403],{},[252,3398,3399],{},"Symptom",[252,3401,3402],{},"Cause",[252,3404,3405],{},"Fix",[259,3407,3408,3425,3438,3449,3462,3475,3486,3497],{},[249,3409,3410,3415,3418],{},[264,3411,3412],{},[14,3413,3414],{},"ValueError: Style X exists already",[264,3416,3417],{},"Theme added twice",[264,3419,3420,3421,3424],{},"Check ",[14,3422,3423],{},"wb.style_names"," before adding.",[249,3426,3427,3430,3435],{},[264,3428,3429],{},"Style raises on a second workbook",[264,3431,3432,3434],{},[14,3433,1785],{}," bound to the first",[264,3436,3437],{},"Build fresh instances via a factory.",[249,3439,3440,3443,3446],{},[264,3441,3442],{},"\"Too many different cell formats\"",[264,3444,3445],{},"Style objects created per cell",[264,3447,3448],{},"Assign named styles by name.",[249,3450,3451,3454,3459],{},[264,3452,3453],{},"Number format lost after re-styling",[264,3455,3456,3458],{},[14,3457,2621],{}," replaces, not merges",[264,3460,3461],{},"Give each role its own number format.",[249,3463,3464,3467,3472],{},[264,3465,3466],{},"Styling vanished after a pandas write",[264,3468,3469,3471],{},[14,3470,3381],{}," replaced the sheet",[264,3473,3474],{},"Apply the theme in a second pass.",[249,3476,3477,3480,3483],{},[264,3478,3479],{},"A column formatted wrongly",[264,3481,3482],{},"Roles keyed by position",[264,3484,3485],{},"Key the mapping on header text.",[249,3487,3488,3491,3494],{},[264,3489,3490],{},"Row-level style ignored",[264,3492,3493],{},"Set on the row dimension",[264,3495,3496],{},"Assign to each cell in the row.",[249,3498,3499,3502,3508],{},[264,3500,3501],{},"Fill renders as nothing",[264,3503,3504,3505,3507],{},"Only ",[14,3506,729],{}," given",[264,3509,3510,3511,231,3513,23],{},"Set both ",[14,3512,729],{},[14,3514,3515],{},"end_color",[189,3517,3519],{"id":3518},"performance-and-scale-notes","Performance and scale notes",[10,3521,3522],{},"Named styles are the cheap path both in memory and in file size, but the per-cell assignment loop still runs in Python. On a hundred-thousand-row sheet that is measurable.",[10,3524,3525,3526,3529],{},"Three ways to reduce it. ",[225,3527,3528],{},"Style only the columns that need a non-default look"," — leaving ordinary text columns unstyled costs nothing and looks fine, so a nine-role theme applied to three of twelve columns does a quarter of the work.",[10,3531,3532,3535,3536,3538],{},[225,3533,3534],{},"Set the workbook's default font"," instead of assigning a ",[14,3537,88],{}," style to every cell:",[194,3540,3542],{"className":355,"code":3541,"language":357,"meta":199,"style":199},"from openpyxl import Workbook\n\nwb = Workbook()\nwb._fonts[0].name = \"Calibri\"     # the default font all unstyled cells inherit\nwb._fonts[0].sz = 11\n",[14,3543,3544,3554,3558,3567,3585],{"__ignoreMap":199},[203,3545,3546,3548,3550,3552],{"class":205,"line":206},[203,3547,372],{"class":371},[203,3549,1897],{"class":375},[203,3551,379],{"class":371},[203,3553,1902],{"class":375},[203,3555,3556],{"class":205,"line":368},[203,3557,401],{"emptyLinePlaceholder":400},[203,3559,3560,3562,3564],{"class":205,"line":385},[203,3561,1923],{"class":375},[203,3563,586],{"class":371},[203,3565,3566],{"class":375}," Workbook()\n",[203,3568,3569,3572,3574,3577,3579,3582],{"class":205,"line":391},[203,3570,3571],{"class":375},"wb._fonts[",[203,3573,47],{"class":407},[203,3575,3576],{"class":375},"].name ",[203,3578,586],{"class":371},[203,3580,3581],{"class":213}," \"Calibri\"",[203,3583,3584],{"class":364},"     # the default font all unstyled cells inherit\n",[203,3586,3587,3589,3591,3594,3596],{"class":205,"line":397},[203,3588,3571],{"class":375},[203,3590,47],{"class":407},[203,3592,3593],{"class":375},"].sz ",[203,3595,586],{"class":371},[203,3597,3598],{"class":407}," 11\n",[10,3600,3601,3604,3605,3608],{},[225,3602,3603],{},"Use xlsxwriter for large new reports."," Its ",[14,3606,3607],{},"set_column"," applies a format to a whole column in one call, independent of row count, which is the single biggest win available:",[194,3610,3612],{"className":355,"code":3611,"language":357,"meta":199,"style":199},"import pandas as pd\n\nTHEME = {\n    \"header\": {\"bold\": True, \"bg_color\": \"#4338CA\", \"font_color\": \"#FFFFFF\",\n               \"border\": 1, \"align\": \"center\", \"valign\": \"vcenter\"},\n    \"currency\": {\"num_format\": \"#,##0.00\", \"align\": \"right\"},\n    \"date\": {\"num_format\": \"yyyy-mm-dd\", \"align\": \"center\"},\n    \"total\": {\"bold\": True, \"bg_color\": \"#D9F4F1\", \"font_color\": \"#0B6157\",\n              \"num_format\": \"#,##0.00\", \"top\": 1},\n}\n\ndef write_large_themed(df, path, roles, sheet_name=\"Report\"):\n    with pd.ExcelWriter(path, engine=\"xlsxwriter\") as writer:\n        df.to_excel(writer, sheet_name=sheet_name, index=False)\n        book, sheet = writer.book, writer.sheets[sheet_name]\n\n        formats = {name: book.add_format(spec) for name, spec in THEME.items()}\n\n        for position, column in enumerate(df.columns):\n            sheet.write(0, position, str(column), formats[\"header\"])\n            role = roles.get(column)\n            sheet.set_column(position, position, 16, formats.get(role))\n\n        sheet.freeze_panes(1, 0)\n    return path\n",[14,3613,3614,3624,3628,3637,3674,3705,3730,3754,3787,3807,3811,3815,3831,3854,3873,3883,3887,3910,3914,3929,3948,3958,3968,3972,3985],{"__ignoreMap":199},[203,3615,3616,3618,3620,3622],{"class":205,"line":206},[203,3617,379],{"class":371},[203,3619,3263],{"class":375},[203,3621,3266],{"class":371},[203,3623,3269],{"class":375},[203,3625,3626],{"class":205,"line":368},[203,3627,401],{"emptyLinePlaceholder":400},[203,3629,3630,3633,3635],{"class":205,"line":385},[203,3631,3632],{"class":407},"THEME",[203,3634,411],{"class":371},[203,3636,414],{"class":375},[203,3638,3639,3642,3645,3648,3650,3652,3654,3657,3659,3662,3664,3667,3669,3672],{"class":205,"line":391},[203,3640,3641],{"class":213},"    \"header\"",[203,3643,3644],{"class":375},": {",[203,3646,3647],{"class":213},"\"bold\"",[203,3649,423],{"class":375},[203,3651,695],{"class":407},[203,3653,592],{"class":375},[203,3655,3656],{"class":213},"\"bg_color\"",[203,3658,423],{"class":375},[203,3660,3661],{"class":213},"\"#4338CA\"",[203,3663,592],{"class":375},[203,3665,3666],{"class":213},"\"font_color\"",[203,3668,423],{"class":375},[203,3670,3671],{"class":213},"\"#FFFFFF\"",[203,3673,429],{"class":375},[203,3675,3676,3679,3681,3683,3685,3688,3690,3692,3694,3697,3699,3702],{"class":205,"line":397},[203,3677,3678],{"class":213},"               \"border\"",[203,3680,423],{"class":375},[203,3682,2128],{"class":407},[203,3684,592],{"class":375},[203,3686,3687],{"class":213},"\"align\"",[203,3689,423],{"class":375},[203,3691,776],{"class":213},[203,3693,592],{"class":375},[203,3695,3696],{"class":213},"\"valign\"",[203,3698,423],{"class":375},[203,3700,3701],{"class":213},"\"vcenter\"",[203,3703,3704],{"class":375},"},\n",[203,3706,3707,3710,3712,3715,3717,3720,3722,3724,3726,3728],{"class":205,"line":404},[203,3708,3709],{"class":213},"    \"currency\"",[203,3711,3644],{"class":375},[203,3713,3714],{"class":213},"\"num_format\"",[203,3716,423],{"class":375},[203,3718,3719],{"class":213},"\"#,##0.00\"",[203,3721,592],{"class":375},[203,3723,3687],{"class":213},[203,3725,423],{"class":375},[203,3727,1229],{"class":213},[203,3729,3704],{"class":375},[203,3731,3732,3735,3737,3739,3741,3744,3746,3748,3750,3752],{"class":205,"line":417},[203,3733,3734],{"class":213},"    \"date\"",[203,3736,3644],{"class":375},[203,3738,3714],{"class":213},[203,3740,423],{"class":375},[203,3742,3743],{"class":213},"\"yyyy-mm-dd\"",[203,3745,592],{"class":375},[203,3747,3687],{"class":213},[203,3749,423],{"class":375},[203,3751,776],{"class":213},[203,3753,3704],{"class":375},[203,3755,3756,3759,3761,3763,3765,3767,3769,3771,3773,3776,3778,3780,3782,3785],{"class":205,"line":432},[203,3757,3758],{"class":213},"    \"total\"",[203,3760,3644],{"class":375},[203,3762,3647],{"class":213},[203,3764,423],{"class":375},[203,3766,695],{"class":407},[203,3768,592],{"class":375},[203,3770,3656],{"class":213},[203,3772,423],{"class":375},[203,3774,3775],{"class":213},"\"#D9F4F1\"",[203,3777,592],{"class":375},[203,3779,3666],{"class":213},[203,3781,423],{"class":375},[203,3783,3784],{"class":213},"\"#0B6157\"",[203,3786,429],{"class":375},[203,3788,3789,3792,3794,3796,3798,3801,3803,3805],{"class":205,"line":445},[203,3790,3791],{"class":213},"              \"num_format\"",[203,3793,423],{"class":375},[203,3795,3719],{"class":213},[203,3797,592],{"class":375},[203,3799,3800],{"class":213},"\"top\"",[203,3802,423],{"class":375},[203,3804,2128],{"class":407},[203,3806,3704],{"class":375},[203,3808,3809],{"class":205,"line":458},[203,3810,552],{"class":375},[203,3812,3813],{"class":205,"line":471},[203,3814,401],{"emptyLinePlaceholder":400},[203,3816,3817,3819,3822,3825,3827,3829],{"class":205,"line":484},[203,3818,624],{"class":371},[203,3820,3821],{"class":627}," write_large_themed",[203,3823,3824],{"class":375},"(df, path, roles, sheet_name",[203,3826,586],{"class":371},[203,3828,3310],{"class":213},[203,3830,2145],{"class":375},[203,3832,3833,3836,3839,3841,3843,3846,3849,3851],{"class":205,"line":497},[203,3834,3835],{"class":371},"    with",[203,3837,3838],{"class":375}," pd.ExcelWriter(path, ",[203,3840,3338],{"class":582},[203,3842,586],{"class":371},[203,3844,3845],{"class":213},"\"xlsxwriter\"",[203,3847,3848],{"class":375},") ",[203,3850,3266],{"class":371},[203,3852,3853],{"class":375}," writer:\n",[203,3855,3856,3859,3861,3863,3865,3867,3869,3871],{"class":205,"line":510},[203,3857,3858],{"class":375},"        df.to_excel(writer, ",[203,3860,3320],{"class":582},[203,3862,586],{"class":371},[203,3864,3325],{"class":375},[203,3866,3328],{"class":582},[203,3868,586],{"class":371},[203,3870,3333],{"class":407},[203,3872,394],{"class":375},[203,3874,3875,3878,3880],{"class":205,"line":523},[203,3876,3877],{"class":375},"        book, sheet ",[203,3879,586],{"class":371},[203,3881,3882],{"class":375}," writer.book, writer.sheets[sheet_name]\n",[203,3884,3885],{"class":205,"line":536},[203,3886,401],{"emptyLinePlaceholder":400},[203,3888,3889,3892,3894,3897,3899,3902,3904,3907],{"class":205,"line":549},[203,3890,3891],{"class":375},"        formats ",[203,3893,586],{"class":371},[203,3895,3896],{"class":375}," {name: book.add_format(spec) ",[203,3898,2745],{"class":371},[203,3900,3901],{"class":375}," name, spec ",[203,3903,1839],{"class":371},[203,3905,3906],{"class":407}," THEME",[203,3908,3909],{"class":375},".items()}\n",[203,3911,3912],{"class":205,"line":555},[203,3913,401],{"emptyLinePlaceholder":400},[203,3915,3916,3918,3921,3923,3926],{"class":205,"line":560},[203,3917,2249],{"class":371},[203,3919,3920],{"class":375}," position, column ",[203,3922,1839],{"class":371},[203,3924,3925],{"class":407}," enumerate",[203,3927,3928],{"class":375},"(df.columns):\n",[203,3930,3931,3934,3936,3939,3941,3944,3946],{"class":205,"line":571},[203,3932,3933],{"class":375},"            sheet.write(",[203,3935,47],{"class":407},[203,3937,3938],{"class":375},", position, ",[203,3940,2210],{"class":407},[203,3942,3943],{"class":375},"(column), formats[",[203,3945,908],{"class":213},[203,3947,608],{"class":375},[203,3949,3950,3953,3955],{"class":205,"line":611},[203,3951,3952],{"class":375},"            role ",[203,3954,586],{"class":371},[203,3956,3957],{"class":375}," roles.get(column)\n",[203,3959,3960,3963,3965],{"class":205,"line":616},[203,3961,3962],{"class":375},"            sheet.set_column(position, position, ",[203,3964,2705],{"class":407},[203,3966,3967],{"class":375},", formats.get(role))\n",[203,3969,3970],{"class":205,"line":621},[203,3971,401],{"emptyLinePlaceholder":400},[203,3973,3974,3977,3979,3981,3983],{"class":205,"line":634},[203,3975,3976],{"class":375},"        sheet.freeze_panes(",[203,3978,2128],{"class":407},[203,3980,592],{"class":375},[203,3982,47],{"class":407},[203,3984,394],{"class":375},[203,3986,3987,3989],{"class":205,"line":640},[203,3988,1766],{"class":371},[203,3990,3375],{"class":375},[10,3992,3993,3994,3997,3998,4001,4002,4005,4006,23],{},"Building the format objects ",[225,3995,3996],{},"once into a dict"," is the same discipline as the named styles — ",[14,3999,4000],{},"add_format"," creates a new entry every call, so constructing one per column in a wide report inflates the workbook's format table for no benefit. And where a report is large enough to need streaming, note that ",[14,4003,4004],{},"write_only"," mode supports styles on the cells you append but not named styles registered afterwards, so the theme must be applied as each row is built — see ",[19,4007,4009],{"href":4008},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-large-excel-files-in-python\u002Fwrite-large-dataframes-to-excel-with-write-only-mode\u002F","writing large DataFrames with write-only mode",[189,4011,4013],{"id":4012},"conclusion","Conclusion",[10,4015,4016,4017,4019],{},"A theme turns styling from something scattered through a script into one module with a palette and a set of roles. Define the roles by meaning — header, currency, total, warning — not by appearance, build them in a factory function so each workbook gets its own ",[14,4018,1785],{}," instances, and register them once with a guard against duplicates. Apply them through a column-name-to-role mapping so an inserted column cannot misformat everything after it. Then never construct a style object inside a loop: that is what produces the 64,000-format error, and a named style avoids it entirely.",[189,4021,4023],{"id":4022},"frequently-asked-questions","Frequently asked questions",[10,4025,4026,4032,4033,4035],{},[225,4027,4028,4029,4031],{},"What is a ",[14,4030,1785],{}," and why use one?","\nA ",[14,4034,1785],{}," is a style registered once on the workbook and referenced by name from any cell. Excel stores it once regardless of how many cells use it, and a reader sees it in the cell-styles gallery, so it is both cheaper and more discoverable than assigning fonts and fills per cell.",[10,4037,4038,4041,4042,4045,4046,4048],{},[225,4039,4040],{},"What causes the \"too many different cell formats\" error?","\nCreating a fresh style object inside a loop. Excel caps a workbook at roughly 64,000 distinct cell formats, and a script that builds a new ",[14,4043,4044],{},"Font"," or ",[14,4047,16],{}," per row reaches it on a large sheet. Define each style once outside the loop.",[10,4050,4051,4057,4058,4060],{},[225,4052,4053,4054,4056],{},"Can I add the same ",[14,4055,1785],{}," to two workbooks?","\nNot the same object — a ",[14,4059,1785],{}," binds to the workbook it is added to. Build them from a plain configuration dictionary with a factory function so each workbook gets its own equivalent instances.",[10,4062,4063,4066],{},[225,4064,4065],{},"How do I apply a style to a whole row or column?","\nThere is no single call; assign the style to each cell in the range. Row and column dimension objects accept a style, but it only affects cells that have never been written to, so it is unreliable for a data region.",[10,4068,4069,4072],{},[225,4070,4071],{},"Should the theme live in code or in a config file?","\nA dictionary in one module is enough for a single project. Move it to a config file when several scripts must match, or when somebody non-technical needs to change the brand colours without touching Python.",[189,4074,4076],{"id":4075},"related","Related",[4078,4079,4080,4087,4096,4103,4110],"ul",{},[4081,4082,4083,4084,4086],"li",{},"Up to the parent: ",[19,4085,22],{"href":21}," — the individual style objects a theme composes.",[4081,4088,4089,4092,4093,4095],{},[19,4090,4091],{"href":3385},"Merge Cells and Centre a Report Title with openpyxl"," — the banner the ",[14,4094,37],{}," role formats.",[4081,4097,4098,4102],{},[19,4099,4101],{"href":4100},"\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"," — the same idea in the other engine.",[4081,4104,4105,4109],{},[19,4106,4108],{"href":4107},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fkeep-excel-report-settings-in-a-config-file\u002F","Keep Excel Report Settings in a Config File"," — moving the palette out of code.",[4081,4111,4112,4116],{},[19,4113,4115],{"href":4114},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fauto-fit-column-widths-when-writing-with-pandas\u002F","Auto-Fit Column Widths When Writing with pandas"," — the sizing pass that pairs with the theme.",[583,4118,4119],{},"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-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}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 .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_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":199,"searchDepth":368,"depth":368,"links":4121},[4122,4123,4124,4125,4126,4127,4128,4129,4130,4131,4132],{"id":191,"depth":368,"text":192},{"id":348,"depth":368,"text":349},{"id":1789,"depth":368,"text":1790},{"id":2016,"depth":368,"text":2017},{"id":2638,"depth":368,"text":2639},{"id":2908,"depth":368,"text":2909},{"id":3389,"depth":368,"text":3390},{"id":3518,"depth":368,"text":3519},{"id":4012,"depth":368,"text":4013},{"id":4022,"depth":368,"text":4023},{"id":4075,"depth":368,"text":4076},"2026-08-15","Stop scattering fonts and fills through your script — define a theme once with NamedStyle, apply it by role, and keep every generated report visually consistent.","md",[4137,4140,4142,4145,4147],{"q":4138,"a":4139},"What is a NamedStyle and why use one?","A NamedStyle is a style registered once on the workbook and referenced by name from any cell. Excel stores it once regardless of how many cells use it, and a reader sees it in the cell-styles gallery, so it is both cheaper and more discoverable than assigning fonts and fills per cell.",{"q":4040,"a":4141},"Creating a fresh style object inside a loop. Excel caps a workbook at roughly 64,000 distinct cell formats, and a script that builds a new Font or PatternFill per row reaches it on a large sheet. Define each style once outside the loop.",{"q":4143,"a":4144},"Can I add the same NamedStyle to two workbooks?","Not the same object — a NamedStyle binds to the workbook it is added to. Build them from a plain configuration dictionary with a factory function so each workbook gets its own equivalent instances.",{"q":4065,"a":4146},"There is no single call; assign the style to each cell in the range. Row and column dimension objects accept a style, but it only affects cells that have never been written to, so it is unreliable for a data region.",{"q":4071,"a":4148},"A dictionary in one module is enough for a single project. Move it to a config file when several scripts must match, or when somebody non-technical needs to change the brand colours without touching Python.",{},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fapply-a-reusable-style-theme-across-an-excel-report",{"title":4152,"description":4153},"Reusable Excel Style Theme in openpyxl (NamedStyle)","Build a report theme in Python: NamedStyle definitions, applying styles by semantic role, avoiding the 64,000 format ceiling, and sharing one theme across several reports.","apply-a-reusable-style-theme-across-an-excel-report","formatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fapply-a-reusable-style-theme-across-an-excel-report\u002Findex","how-to","DKvdgfv3mcfyb356bd0kJ6wvOGVMvVaU0qv1spg8rIY",[4159,4162],{"title":22,"path":4160,"stem":4161,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Findex",{"title":4163,"path":4164,"stem":4165,"children":-1},"Freeze the Header Row in Excel with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Ffreeze-header-row-in-excel-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Ffreeze-header-row-in-excel-with-openpyxl\u002Findex",1786800028659]