[{"data":1,"prerenderedAt":2445},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-borders-to-a-range-with-openpyxl":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-borders-to-a-range-with-openpyxl":2413,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-borders-to-a-range-with-openpyxl":2421,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-borders-to-a-range-with-openpyxl":2426,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-borders-to-a-range-with-openpyxl":2427},{"id":4,"title":5,"body":6,"dateModified":2379,"datePublished":2379,"description":2380,"extension":2381,"faq":2382,"keyword":2391,"kind":2392,"meta":2393,"navigation":306,"path":2402,"seo":2403,"slug":2406,"stem":2407,"topics":2408,"type":2411,"__hash__":2412},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-borders-to-a-range-with-openpyxl\u002Findex.md","Add Borders to a Range with openpyxl",{"type":7,"value":8,"toc":2366},"minimark",[9,24,245,250,275,278,516,520,534,737,740,744,750,1118,1127,1398,1402,1405,1577,1580,1584,1591,1707,1717,1811,1815,1818,1907,1916,1920,1927,2230,2243,2247,2286,2290,2302,2306,2312,2318,2324,2330,2334,2363],[10,11,12,13,17,18,23],"p",{},"Borders make a table readable: a light grid to guide the eye, a line under the header, a box around the whole table and the classic double rule above a total. In Excel you select a range and click \"Outside Borders\". openpyxl has no such command — borders belong to individual cells, and each cell's ",[14,15,16],"code",{},"border"," is replaced wholesale on every assignment. That is why border code so often erases itself. This guide, part of ",[19,20,22],"a",{"href":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002F","Styling Excel Cells with openpyxl",", builds a few small helpers that apply borders to ranges the way Excel does, and checks the result by rendering the workbook.",[25,26,34,35,34,39,34,43,34,51,34,60,34,64,34,71,34,75,34,79,34,83,34,87,34,91,34,95,34,99,34,104,34,109,34,113,34,115,34,118,34,120,34,123,34,125,34,128,34,131,34,135,34,137,34,141,34,143,34,146,34,148,34,151,34,153,34,156,34,159,34,163,34,165,34,168,34,170,34,173,34,175,34,178,34,180,34,183,34,186,34,190,34,192,34,195,34,197,34,200,34,202,34,205,34,207,34,210,34,213,34,217,34,221,34,224,34,226,34,229,34,231,34,234,34,236,34,239],"svg",{"viewBox":27,"role":28,"ariaLabelledBy":29,"xmlns":32,"style":33},"233 0 293 222","img",[30,31],"bd-sheet-t","bd-sheet-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:293px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[36,37,38],"title",{"id":30},"A table with grid, header rule, outline and total rule",[40,41,42],"desc",{"id":31},"A sales table with Region, Q1, Q2 and Total columns. Light grey grid lines separate all cells, a medium dark line runs under the header, a medium outline surrounds the table, and a double line sits above the Total row with its sums of 240, 285 and 525.",[44,45],"rect",{"x":46,"y":47,"width":48,"height":49,"fill":50},"233","0","293","222","#ffffff",[44,52],{"x":53,"y":54,"width":55,"height":56,"rx":47,"fill":57,"stroke":58,"style":59},"254.4","16.0","32.0","24.0","#e7ebef","var(--line,#cdd5e6)","stroke-width:1px",[44,61],{"x":62,"y":54,"width":63,"height":56,"rx":47,"fill":57,"stroke":58,"style":59},"286.4","66.7",[65,66,70],"text",{"x":67,"y":68,"style":69},"319.8","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[44,72],{"x":73,"y":54,"width":74,"height":56,"rx":47,"fill":57,"stroke":58,"style":59},"353.2","48.9",[65,76,78],{"x":77,"y":68,"style":69},"377.6","B",[44,80],{"x":81,"y":54,"width":82,"height":56,"rx":47,"fill":57,"stroke":58,"style":59},"402.1","48.0",[65,84,86],{"x":85,"y":68,"style":69},"426.1","C",[44,88],{"x":89,"y":54,"width":90,"height":56,"rx":47,"fill":57,"stroke":58,"style":59},"450.1","55.5",[65,92,94],{"x":93,"y":68,"style":69},"477.8","D",[44,96],{"x":53,"y":97,"width":55,"height":98,"rx":47,"fill":57,"stroke":58,"style":59},"40.0","28.0",[65,100,103],{"x":101,"y":102,"style":69},"270.4","59.0","1",[44,105],{"x":62,"y":97,"width":63,"height":98,"rx":47,"fill":106,"stroke":107,"style":108},"#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.5px",[65,110,112],{"x":67,"y":102,"style":111},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Region",[44,114],{"x":73,"y":97,"width":74,"height":98,"rx":47,"fill":106,"stroke":107,"style":108},[65,116,117],{"x":77,"y":102,"style":111},"Q1",[44,119],{"x":81,"y":97,"width":82,"height":98,"rx":47,"fill":106,"stroke":107,"style":108},[65,121,122],{"x":85,"y":102,"style":111},"Q2",[44,124],{"x":89,"y":97,"width":90,"height":98,"rx":47,"fill":106,"stroke":107,"style":108},[65,126,127],{"x":93,"y":102,"style":111},"Total",[44,129],{"x":53,"y":130,"width":55,"height":98,"rx":47,"fill":57,"stroke":58,"style":59},"68.0",[65,132,134],{"x":101,"y":133,"style":69},"87.0","2",[44,136],{"x":62,"y":130,"width":63,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,138,140],{"x":67,"y":133,"style":139},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","North",[44,142],{"x":73,"y":130,"width":74,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,144,145],{"x":77,"y":133,"style":139},"120",[44,147],{"x":81,"y":130,"width":82,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,149,150],{"x":85,"y":133,"style":139},"140",[44,152],{"x":89,"y":130,"width":90,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,154,155],{"x":93,"y":133,"style":139},"260",[44,157],{"x":53,"y":158,"width":55,"height":98,"rx":47,"fill":57,"stroke":58,"style":59},"96.0",[65,160,162],{"x":101,"y":161,"style":69},"115.0","3",[44,164],{"x":62,"y":158,"width":63,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,166,167],{"x":67,"y":161,"style":139},"South",[44,169],{"x":73,"y":158,"width":74,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,171,172],{"x":77,"y":161,"style":139},"80",[44,174],{"x":81,"y":158,"width":82,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,176,177],{"x":85,"y":161,"style":139},"90",[44,179],{"x":89,"y":158,"width":90,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,181,182],{"x":93,"y":161,"style":139},"170",[44,184],{"x":53,"y":185,"width":55,"height":98,"rx":47,"fill":57,"stroke":58,"style":59},"124.0",[65,187,189],{"x":101,"y":188,"style":69},"143.0","4",[44,191],{"x":62,"y":185,"width":63,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,193,194],{"x":67,"y":188,"style":139},"East",[44,196],{"x":73,"y":185,"width":74,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,198,199],{"x":77,"y":188,"style":139},"40",[44,201],{"x":81,"y":185,"width":82,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,203,204],{"x":85,"y":188,"style":139},"55",[44,206],{"x":89,"y":185,"width":90,"height":98,"rx":47,"fill":50,"stroke":58,"style":59},[65,208,209],{"x":93,"y":188,"style":139},"95",[44,211],{"x":53,"y":212,"width":55,"height":98,"rx":47,"fill":57,"stroke":58,"style":59},"152.0",[65,214,216],{"x":101,"y":215,"style":69},"171.0","5",[44,218],{"x":62,"y":212,"width":63,"height":98,"rx":47,"fill":219,"stroke":220,"style":108},"#d9f4f1","var(--teal,#0f9488)",[65,222,127],{"x":67,"y":215,"style":223},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[44,225],{"x":73,"y":212,"width":74,"height":98,"rx":47,"fill":219,"stroke":220,"style":108},[65,227,228],{"x":77,"y":215,"style":223},"240",[44,230],{"x":81,"y":212,"width":82,"height":98,"rx":47,"fill":219,"stroke":220,"style":108},[65,232,233],{"x":85,"y":215,"style":223},"285",[44,235],{"x":89,"y":212,"width":90,"height":98,"rx":47,"fill":219,"stroke":220,"style":108},[65,237,238],{"x":93,"y":215,"style":223},"525",[65,240,244],{"x":241,"y":242,"style":243},"380.0","202.0","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","grid first, then the rules that sit on top of it",[246,247,249],"h2",{"id":248},"prerequisites","Prerequisites",[251,252,257],"pre",{"className":253,"code":254,"language":255,"meta":256,"style":256},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl\n","bash","",[14,258,259],{"__ignoreMap":256},[260,261,264,268,272],"span",{"class":262,"line":263},"line",1,[260,265,267],{"class":266},"sMTad","pip",[260,269,271],{"class":270},"srMev"," install",[260,273,274],{"class":270}," openpyxl\n",[10,276,277],{},"The sample table:",[251,279,283],{"className":280,"code":281,"language":282,"meta":256,"style":256},"language-python shiki shiki-themes github-light github-dark-high-contrast","from copy import copy\n\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Border, Font, Side\nfrom openpyxl.utils.cell import range_boundaries\n\nwb = Workbook()\nws = wb.active\nws.title = \"Sales\"\nfor row in [(\"Region\", \"Q1\", \"Q2\", \"Total\"), (\"North\", 120, 140, \"=B2+C2\"),\n            (\"South\", 80, 90, \"=B3+C3\"), (\"East\", 40, 55, \"=B4+C4\"),\n            (\"Total\", \"=SUM(B2:B4)\", \"=SUM(C2:C4)\", \"=SUM(D2:D4)\")]:\n    ws.append(row)\n","python",[14,284,285,301,308,321,334,347,352,364,375,386,443,485,510],{"__ignoreMap":256},[260,286,287,291,295,298],{"class":262,"line":263},[260,288,290],{"class":289},"s-kum","from",[260,292,294],{"class":293},"skGVy"," copy ",[260,296,297],{"class":289},"import",[260,299,300],{"class":293}," copy\n",[260,302,304],{"class":262,"line":303},2,[260,305,307],{"emptyLinePlaceholder":306},true,"\n",[260,309,311,313,316,318],{"class":262,"line":310},3,[260,312,290],{"class":289},[260,314,315],{"class":293}," openpyxl ",[260,317,297],{"class":289},[260,319,320],{"class":293}," Workbook\n",[260,322,324,326,329,331],{"class":262,"line":323},4,[260,325,290],{"class":289},[260,327,328],{"class":293}," openpyxl.styles ",[260,330,297],{"class":289},[260,332,333],{"class":293}," Border, Font, Side\n",[260,335,337,339,342,344],{"class":262,"line":336},5,[260,338,290],{"class":289},[260,340,341],{"class":293}," openpyxl.utils.cell ",[260,343,297],{"class":289},[260,345,346],{"class":293}," range_boundaries\n",[260,348,350],{"class":262,"line":349},6,[260,351,307],{"emptyLinePlaceholder":306},[260,353,355,358,361],{"class":262,"line":354},7,[260,356,357],{"class":293},"wb ",[260,359,360],{"class":289},"=",[260,362,363],{"class":293}," Workbook()\n",[260,365,367,370,372],{"class":262,"line":366},8,[260,368,369],{"class":293},"ws ",[260,371,360],{"class":289},[260,373,374],{"class":293}," wb.active\n",[260,376,378,381,383],{"class":262,"line":377},9,[260,379,380],{"class":293},"ws.title ",[260,382,360],{"class":289},[260,384,385],{"class":270}," \"Sales\"\n",[260,387,389,392,395,398,401,404,407,410,412,415,417,420,423,426,428,431,433,435,437,440],{"class":262,"line":388},10,[260,390,391],{"class":289},"for",[260,393,394],{"class":293}," row ",[260,396,397],{"class":289},"in",[260,399,400],{"class":293}," [(",[260,402,403],{"class":270},"\"Region\"",[260,405,406],{"class":293},", ",[260,408,409],{"class":270},"\"Q1\"",[260,411,406],{"class":293},[260,413,414],{"class":270},"\"Q2\"",[260,416,406],{"class":293},[260,418,419],{"class":270},"\"Total\"",[260,421,422],{"class":293},"), (",[260,424,425],{"class":270},"\"North\"",[260,427,406],{"class":293},[260,429,145],{"class":430},"sP0c6",[260,432,406],{"class":293},[260,434,150],{"class":430},[260,436,406],{"class":293},[260,438,439],{"class":270},"\"=B2+C2\"",[260,441,442],{"class":293},"),\n",[260,444,446,449,452,454,456,458,460,462,465,467,470,472,474,476,478,480,483],{"class":262,"line":445},11,[260,447,448],{"class":293},"            (",[260,450,451],{"class":270},"\"South\"",[260,453,406],{"class":293},[260,455,172],{"class":430},[260,457,406],{"class":293},[260,459,177],{"class":430},[260,461,406],{"class":293},[260,463,464],{"class":270},"\"=B3+C3\"",[260,466,422],{"class":293},[260,468,469],{"class":270},"\"East\"",[260,471,406],{"class":293},[260,473,199],{"class":430},[260,475,406],{"class":293},[260,477,204],{"class":430},[260,479,406],{"class":293},[260,481,482],{"class":270},"\"=B4+C4\"",[260,484,442],{"class":293},[260,486,488,490,492,494,497,499,502,504,507],{"class":262,"line":487},12,[260,489,448],{"class":293},[260,491,419],{"class":270},[260,493,406],{"class":293},[260,495,496],{"class":270},"\"=SUM(B2:B4)\"",[260,498,406],{"class":293},[260,500,501],{"class":270},"\"=SUM(C2:C4)\"",[260,503,406],{"class":293},[260,505,506],{"class":270},"\"=SUM(D2:D4)\"",[260,508,509],{"class":293},")]:\n",[260,511,513],{"class":262,"line":512},13,[260,514,515],{"class":293},"    ws.append(row)\n",[246,517,519],{"id":518},"step-1-borders-are-four-sides-replaced-together","Step 1 — Borders are four Sides, replaced together",[10,521,522,523,526,527,530,531,533],{},"A ",[14,524,525],{},"Border"," holds four ",[14,528,529],{},"Side"," objects — left, right, top, bottom — each with a style and a colour. Assigning a new ",[14,532,525],{}," to a cell replaces all four at once, and style objects cannot be edited in place:",[251,535,537],{"className":280,"code":536,"language":282,"meta":256,"style":256},"thin = Side(style=\"thin\", color=\"CDD5E6\")\nws[\"A1\"].border = Border(left=thin, right=thin, top=thin, bottom=thin)\nws[\"A1\"].border = Border(bottom=Side(style=\"medium\"))\nprint(getattr(ws[\"A1\"].border.left, \"style\", None), ws[\"A1\"].border.bottom.style)     # None medium\n\ntry:\n    ws[\"A1\"].border.left = thin\nexcept AttributeError as exc:\n    print(exc)          # Style objects are immutable and cannot be changed.Reassign the style with a copy\n",[14,538,539,571,617,646,685,689,697,712,726],{"__ignoreMap":256},[260,540,541,544,546,549,553,555,558,560,563,565,568],{"class":262,"line":263},[260,542,543],{"class":293},"thin ",[260,545,360],{"class":289},[260,547,548],{"class":293}," Side(",[260,550,552],{"class":551},"sa561","style",[260,554,360],{"class":289},[260,556,557],{"class":270},"\"thin\"",[260,559,406],{"class":293},[260,561,562],{"class":551},"color",[260,564,360],{"class":289},[260,566,567],{"class":270},"\"CDD5E6\"",[260,569,570],{"class":293},")\n",[260,572,573,576,579,582,584,587,590,592,595,598,600,602,605,607,609,612,614],{"class":262,"line":303},[260,574,575],{"class":293},"ws[",[260,577,578],{"class":270},"\"A1\"",[260,580,581],{"class":293},"].border ",[260,583,360],{"class":289},[260,585,586],{"class":293}," Border(",[260,588,589],{"class":551},"left",[260,591,360],{"class":289},[260,593,594],{"class":293},"thin, ",[260,596,597],{"class":551},"right",[260,599,360],{"class":289},[260,601,594],{"class":293},[260,603,604],{"class":551},"top",[260,606,360],{"class":289},[260,608,594],{"class":293},[260,610,611],{"class":551},"bottom",[260,613,360],{"class":289},[260,615,616],{"class":293},"thin)\n",[260,618,619,621,623,625,627,629,631,633,636,638,640,643],{"class":262,"line":310},[260,620,575],{"class":293},[260,622,578],{"class":270},[260,624,581],{"class":293},[260,626,360],{"class":289},[260,628,586],{"class":293},[260,630,611],{"class":551},[260,632,360],{"class":289},[260,634,635],{"class":293},"Side(",[260,637,552],{"class":551},[260,639,360],{"class":289},[260,641,642],{"class":270},"\"medium\"",[260,644,645],{"class":293},"))\n",[260,647,648,651,654,657,660,662,665,668,670,673,676,678,681],{"class":262,"line":323},[260,649,650],{"class":430},"print",[260,652,653],{"class":293},"(",[260,655,656],{"class":430},"getattr",[260,658,659],{"class":293},"(ws[",[260,661,578],{"class":270},[260,663,664],{"class":293},"].border.left, ",[260,666,667],{"class":270},"\"style\"",[260,669,406],{"class":293},[260,671,672],{"class":430},"None",[260,674,675],{"class":293},"), ws[",[260,677,578],{"class":270},[260,679,680],{"class":293},"].border.bottom.style)     ",[260,682,684],{"class":683},"s-wDw","# None medium\n",[260,686,687],{"class":262,"line":336},[260,688,307],{"emptyLinePlaceholder":306},[260,690,691,694],{"class":262,"line":349},[260,692,693],{"class":289},"try",[260,695,696],{"class":293},":\n",[260,698,699,702,704,707,709],{"class":262,"line":354},[260,700,701],{"class":293},"    ws[",[260,703,578],{"class":270},[260,705,706],{"class":293},"].border.left ",[260,708,360],{"class":289},[260,710,711],{"class":293}," thin\n",[260,713,714,717,720,723],{"class":262,"line":366},[260,715,716],{"class":289},"except",[260,718,719],{"class":430}," AttributeError",[260,721,722],{"class":289}," as",[260,724,725],{"class":293}," exc:\n",[260,727,728,731,734],{"class":262,"line":377},[260,729,730],{"class":430},"    print",[260,732,733],{"class":293},"(exc)          ",[260,735,736],{"class":683},"# Style objects are immutable and cannot be changed.Reassign the style with a copy\n",[10,738,739],{},"The second assignment wiped the left, right and top edges — the classic self-erasing border. The fix is a helper that starts from the cell's current border and replaces only the edges you name.",[246,741,743],{"id":742},"step-2-a-helper-that-keeps-existing-edges","Step 2 — A helper that keeps existing edges",[10,745,746,749],{},[14,747,748],{},"set_edges"," reads the cell's current border, swaps in the given sides and assigns the result:",[251,751,753],{"className":280,"code":752,"language":282,"meta":256,"style":256},"def set_edges(cell, **sides):\n    current = cell.border\n    cell.border = Border(\n        left=sides.get(\"left\", current.left), right=sides.get(\"right\", current.right),\n        top=sides.get(\"top\", current.top), bottom=sides.get(\"bottom\", current.bottom),\n    )\n\ndef grid(ws, ref, side):\n    for row in ws[ref]:\n        for cell in row:\n            set_edges(cell, left=side, right=side, top=side, bottom=side)\n\ndef outline(ws, ref, side):\n    min_col, min_row, max_col, max_row = range_boundaries(ref)\n    for row in ws.iter_rows(min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col):\n        for cell in row:\n            edges = {}\n            if cell.row == min_row: edges[\"top\"] = side\n            if cell.row == max_row: edges[\"bottom\"] = side\n            if cell.column == min_col: edges[\"left\"] = side\n            if cell.column == max_col: edges[\"right\"] = side\n            if edges:\n                set_edges(cell, **edges)\n",[14,754,755,773,783,793,821,848,853,857,867,879,892,923,927,936,947,991,1002,1013,1038,1058,1079,1099,1107],{"__ignoreMap":256},[260,756,757,760,764,767,770],{"class":262,"line":263},[260,758,759],{"class":289},"def",[260,761,763],{"class":762},"s_Opv"," set_edges",[260,765,766],{"class":293},"(cell, ",[260,768,769],{"class":289},"**",[260,771,772],{"class":293},"sides):\n",[260,774,775,778,780],{"class":262,"line":303},[260,776,777],{"class":293},"    current ",[260,779,360],{"class":289},[260,781,782],{"class":293}," cell.border\n",[260,784,785,788,790],{"class":262,"line":310},[260,786,787],{"class":293},"    cell.border ",[260,789,360],{"class":289},[260,791,792],{"class":293}," Border(\n",[260,794,795,798,800,803,806,809,811,813,815,818],{"class":262,"line":323},[260,796,797],{"class":551},"        left",[260,799,360],{"class":289},[260,801,802],{"class":293},"sides.get(",[260,804,805],{"class":270},"\"left\"",[260,807,808],{"class":293},", current.left), ",[260,810,597],{"class":551},[260,812,360],{"class":289},[260,814,802],{"class":293},[260,816,817],{"class":270},"\"right\"",[260,819,820],{"class":293},", current.right),\n",[260,822,823,826,828,830,833,836,838,840,842,845],{"class":262,"line":336},[260,824,825],{"class":551},"        top",[260,827,360],{"class":289},[260,829,802],{"class":293},[260,831,832],{"class":270},"\"top\"",[260,834,835],{"class":293},", current.top), ",[260,837,611],{"class":551},[260,839,360],{"class":289},[260,841,802],{"class":293},[260,843,844],{"class":270},"\"bottom\"",[260,846,847],{"class":293},", current.bottom),\n",[260,849,850],{"class":262,"line":349},[260,851,852],{"class":293},"    )\n",[260,854,855],{"class":262,"line":354},[260,856,307],{"emptyLinePlaceholder":306},[260,858,859,861,864],{"class":262,"line":366},[260,860,759],{"class":289},[260,862,863],{"class":762}," grid",[260,865,866],{"class":293},"(ws, ref, side):\n",[260,868,869,872,874,876],{"class":262,"line":377},[260,870,871],{"class":289},"    for",[260,873,394],{"class":293},[260,875,397],{"class":289},[260,877,878],{"class":293}," ws[ref]:\n",[260,880,881,884,887,889],{"class":262,"line":388},[260,882,883],{"class":289},"        for",[260,885,886],{"class":293}," cell ",[260,888,397],{"class":289},[260,890,891],{"class":293}," row:\n",[260,893,894,897,899,901,904,906,908,910,912,914,916,918,920],{"class":262,"line":445},[260,895,896],{"class":293},"            set_edges(cell, ",[260,898,589],{"class":551},[260,900,360],{"class":289},[260,902,903],{"class":293},"side, ",[260,905,597],{"class":551},[260,907,360],{"class":289},[260,909,903],{"class":293},[260,911,604],{"class":551},[260,913,360],{"class":289},[260,915,903],{"class":293},[260,917,611],{"class":551},[260,919,360],{"class":289},[260,921,922],{"class":293},"side)\n",[260,924,925],{"class":262,"line":487},[260,926,307],{"emptyLinePlaceholder":306},[260,928,929,931,934],{"class":262,"line":512},[260,930,759],{"class":289},[260,932,933],{"class":762}," outline",[260,935,866],{"class":293},[260,937,939,942,944],{"class":262,"line":938},14,[260,940,941],{"class":293},"    min_col, min_row, max_col, max_row ",[260,943,360],{"class":289},[260,945,946],{"class":293}," range_boundaries(ref)\n",[260,948,950,952,954,956,959,962,964,967,970,972,975,978,980,983,986,988],{"class":262,"line":949},15,[260,951,871],{"class":289},[260,953,394],{"class":293},[260,955,397],{"class":289},[260,957,958],{"class":293}," ws.iter_rows(",[260,960,961],{"class":551},"min_row",[260,963,360],{"class":289},[260,965,966],{"class":293},"min_row, ",[260,968,969],{"class":551},"max_row",[260,971,360],{"class":289},[260,973,974],{"class":293},"max_row, ",[260,976,977],{"class":551},"min_col",[260,979,360],{"class":289},[260,981,982],{"class":293},"min_col, ",[260,984,985],{"class":551},"max_col",[260,987,360],{"class":289},[260,989,990],{"class":293},"max_col):\n",[260,992,994,996,998,1000],{"class":262,"line":993},16,[260,995,883],{"class":289},[260,997,886],{"class":293},[260,999,397],{"class":289},[260,1001,891],{"class":293},[260,1003,1005,1008,1010],{"class":262,"line":1004},17,[260,1006,1007],{"class":293},"            edges ",[260,1009,360],{"class":289},[260,1011,1012],{"class":293}," {}\n",[260,1014,1016,1019,1022,1025,1028,1030,1033,1035],{"class":262,"line":1015},18,[260,1017,1018],{"class":289},"            if",[260,1020,1021],{"class":293}," cell.row ",[260,1023,1024],{"class":289},"==",[260,1026,1027],{"class":293}," min_row: edges[",[260,1029,832],{"class":270},[260,1031,1032],{"class":293},"] ",[260,1034,360],{"class":289},[260,1036,1037],{"class":293}," side\n",[260,1039,1041,1043,1045,1047,1050,1052,1054,1056],{"class":262,"line":1040},19,[260,1042,1018],{"class":289},[260,1044,1021],{"class":293},[260,1046,1024],{"class":289},[260,1048,1049],{"class":293}," max_row: edges[",[260,1051,844],{"class":270},[260,1053,1032],{"class":293},[260,1055,360],{"class":289},[260,1057,1037],{"class":293},[260,1059,1061,1063,1066,1068,1071,1073,1075,1077],{"class":262,"line":1060},20,[260,1062,1018],{"class":289},[260,1064,1065],{"class":293}," cell.column ",[260,1067,1024],{"class":289},[260,1069,1070],{"class":293}," min_col: edges[",[260,1072,805],{"class":270},[260,1074,1032],{"class":293},[260,1076,360],{"class":289},[260,1078,1037],{"class":293},[260,1080,1082,1084,1086,1088,1091,1093,1095,1097],{"class":262,"line":1081},21,[260,1083,1018],{"class":289},[260,1085,1065],{"class":293},[260,1087,1024],{"class":289},[260,1089,1090],{"class":293}," max_col: edges[",[260,1092,817],{"class":270},[260,1094,1032],{"class":293},[260,1096,360],{"class":289},[260,1098,1037],{"class":293},[260,1100,1102,1104],{"class":262,"line":1101},22,[260,1103,1018],{"class":289},[260,1105,1106],{"class":293}," edges:\n",[260,1108,1110,1113,1115],{"class":262,"line":1109},23,[260,1111,1112],{"class":293},"                set_edges(cell, ",[260,1114,769],{"class":289},[260,1116,1117],{"class":293},"edges)\n",[10,1119,1120,1123,1124,1126],{},[14,1121,1122],{},"outline"," touches only the cells on the edge of the range and only the edges that face outward, which is exactly what Excel's \"Outside Borders\" does. Because every change goes through ",[14,1125,748],{},", the helpers can be applied in any order without erasing each other.",[25,1128,34,1134,34,1137,34,1140,34,1144,34,1151,34,1156,34,1194,34,1196,34,1205,34,1208,34,1229,34,1232,34,1235,34,1237,34,1257,34,1260,34,1264,34,1266,34,1301,34,1304,34,1336,34,1339,34,1351,34,1357,34,1360,34,1363,34,1367,34,1370,34,1373,34,1375,34,1379,34,1383,34,1386,34,1389,34,1391,34,1395],{"viewBox":1129,"role":28,"ariaLabelledBy":1130,"xmlns":32,"style":1133},"4 0 752 248",[1131,1132],"bd-helper-t","bd-helper-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[36,1135,1136],{"id":1131},"Why set_edges does not erase earlier borders",[40,1138,1139],{"id":1132},"The helper reads the cell's current border, keeps every side that was not passed in, replaces only the named sides, and assigns a new Border. Assigning Border with only a bottom side would instead reset left, right and top to nothing.",[44,1141],{"x":189,"y":47,"width":1142,"height":1143,"fill":50},"752","248",[44,1145],{"x":1146,"y":54,"width":1147,"height":1148,"rx":1149,"fill":1150,"stroke":58,"style":108},"20.0","720.0","150.0","12","#f0f2f5",[65,1152,103],{"x":1153,"y":1154,"style":1155},"60.0","47.0","font-size:11px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",[65,1157,1160,1164,1168,1170,1172,1175,1178,1180,1183,1185,1188,1191],{"x":1158,"y":1154,"style":1159},"82.0","font-size:12.5px;text-anchor:start;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace",[1161,1162,759],"tspan",{"style":1163},"fill:var(--brand-strong,#4338ca);font-weight:700",[1161,1165,1167],{"style":1166},"fill:var(--text,#172033);font-weight:400"," ",[1161,1169,748],{"style":1166},[1161,1171,653],{"style":1166},[1161,1173,1174],{"style":1166},"cell",[1161,1176,1177],{"style":1166},",",[1161,1179,1167],{"style":1166},[1161,1181,1182],{"style":1166},"*",[1161,1184,1182],{"style":1166},[1161,1186,1187],{"style":1166},"sides",[1161,1189,1190],{"style":1166},")",[1161,1192,1193],{"style":1166},":",[65,1195,134],{"x":1153,"y":130,"style":1155},[1197,1198],"circle",{"cx":1199,"cy":1200,"r":1201,"fill":1202,"stroke":1203,"style":1204},"36.0","63.5","11.0","#4338ca","none","stroke-width:0px",[65,1206,103],{"x":1199,"y":130,"style":1207},"font-size:12px;font-weight:700;fill:#ffffff;text-anchor:middle",[65,1209,1210,1213,1216,1218,1220,1222,1224,1227],{"x":1158,"y":130,"style":1159},[1161,1211,1212],{"style":1166},"    ",[1161,1214,1215],{"style":1166},"current",[1161,1217,1167],{"style":1166},[1161,1219,360],{"style":1166},[1161,1221,1167],{"style":1166},[1161,1223,1174],{"style":1166},[1161,1225,1226],{"style":1166},".",[1161,1228,16],{"style":1166},[65,1230,162],{"x":1153,"y":1231,"style":1155},"89.0",[1197,1233],{"cx":1199,"cy":1234,"r":1201,"fill":1202,"stroke":1203,"style":1204},"84.5",[65,1236,162],{"x":1199,"y":1231,"style":1207},[65,1238,1239,1241,1243,1245,1247,1249,1251,1253,1255],{"x":1158,"y":1231,"style":1159},[1161,1240,1212],{"style":1166},[1161,1242,1174],{"style":1166},[1161,1244,1226],{"style":1166},[1161,1246,16],{"style":1166},[1161,1248,1167],{"style":1166},[1161,1250,360],{"style":1166},[1161,1252,1167],{"style":1166},[1161,1254,525],{"style":1166},[1161,1256,653],{"style":1166},[65,1258,189],{"x":1153,"y":1259,"style":1155},"110.0",[1197,1261],{"cx":1199,"cy":1262,"r":1201,"fill":1263,"stroke":1203,"style":1204},"105.5","#0b6157",[65,1265,134],{"x":1199,"y":1259,"style":1207},[65,1267,1268,1271,1273,1275,1277,1279,1282,1284,1287,1289,1291,1293,1295,1297,1299],{"x":1158,"y":1259,"style":1159},[1161,1269,1270],{"style":1166},"        ",[1161,1272,589],{"style":1166},[1161,1274,360],{"style":1166},[1161,1276,1187],{"style":1166},[1161,1278,1226],{"style":1166},[1161,1280,1281],{"style":1166},"get",[1161,1283,653],{"style":1166},[1161,1285,805],{"style":1286},"fill:var(--teal-ink,#0b6157);font-weight:400",[1161,1288,1177],{"style":1166},[1161,1290,1167],{"style":1166},[1161,1292,1215],{"style":1166},[1161,1294,1226],{"style":1166},[1161,1296,589],{"style":1166},[1161,1298,1190],{"style":1166},[1161,1300,1177],{"style":1166},[65,1302,216],{"x":1153,"y":1303,"style":1155},"131.0",[65,1305,1306,1308,1310,1312,1314,1316,1318,1320,1322,1324,1326,1328,1330,1332,1334],{"x":1158,"y":1303,"style":1159},[1161,1307,1270],{"style":1166},[1161,1309,611],{"style":1166},[1161,1311,360],{"style":1166},[1161,1313,1187],{"style":1166},[1161,1315,1226],{"style":1166},[1161,1317,1281],{"style":1166},[1161,1319,653],{"style":1166},[1161,1321,844],{"style":1286},[1161,1323,1177],{"style":1166},[1161,1325,1167],{"style":1166},[1161,1327,1215],{"style":1166},[1161,1329,1226],{"style":1166},[1161,1331,611],{"style":1166},[1161,1333,1190],{"style":1166},[1161,1335,1177],{"style":1166},[65,1337,1338],{"x":1153,"y":212,"style":1155},"6",[65,1340,1341,1343,1345,1347,1349],{"x":1158,"y":212,"style":1159},[1161,1342,1270],{"style":1166},[1161,1344,1226],{"style":1166},[1161,1346,1226],{"style":1166},[1161,1348,1226],{"style":1166},[1161,1350,1190],{"style":1166},[44,1352],{"x":1146,"y":1353,"width":1354,"height":1355,"rx":1356,"fill":106,"stroke":107,"style":108},"180.0","230.7","52.0","10",[1197,1358],{"cx":97,"cy":1359,"r":1201,"fill":1202,"stroke":1203,"style":1204},"206.0",[65,1361,103],{"x":97,"y":1362,"style":1207},"210.5",[65,1364,1366],{"x":1153,"y":1362,"style":1365},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","read what is already there",[44,1368],{"x":1369,"y":1353,"width":1354,"height":1355,"rx":1356,"fill":219,"stroke":220,"style":108},"264.7",[1197,1371],{"cx":1372,"cy":1359,"r":1201,"fill":1263,"stroke":1203,"style":1204},"284.7",[65,1374,134],{"x":1372,"y":1362,"style":1207},[65,1376,1378],{"x":1377,"y":242,"style":1365},"304.7","keep an edge unless it was",[65,1380,1382],{"x":1377,"y":1381,"style":1365},"219.0","passed in",[44,1384],{"x":1385,"y":1353,"width":1354,"height":1355,"rx":1356,"fill":106,"stroke":107,"style":108},"509.3",[1197,1387],{"cx":1388,"cy":1359,"r":1201,"fill":1202,"stroke":1203,"style":1204},"529.3",[65,1390,162],{"x":1388,"y":1362,"style":1207},[65,1392,1394],{"x":1393,"y":242,"style":1365},"549.3","one new Border, assigned",[65,1396,1397],{"x":1393,"y":1381,"style":1365},"once",[246,1399,1401],{"id":1400},"step-3-layer-the-tables-borders","Step 3 — Layer the table's borders",[10,1403,1404],{},"Apply the light grid first, then the rules that should sit on top of it, then the outline last so nothing overwrites it:",[251,1406,1408],{"className":280,"code":1407,"language":282,"meta":256,"style":256},"medium = Side(style=\"medium\", color=\"1E293B\")\ndouble = Side(style=\"double\", color=\"1E293B\")\n\ngrid(ws, \"A1:D5\", thin)\nfor cell in ws[1]:\n    set_edges(cell, bottom=medium)\nfor cell in ws[5]:\n    set_edges(cell, top=double)\noutline(ws, \"A1:D5\", medium)\nwb.save(\"bordered.xlsx\")\nprint(ws[\"A1\"].border.left.style, ws[\"B3\"].border.left.style, ws[\"B5\"].border.top.style)\n# medium thin double\n",[14,1409,1410,1436,1462,1466,1477,1493,1505,1519,1530,1540,1550,1572],{"__ignoreMap":256},[260,1411,1412,1415,1417,1419,1421,1423,1425,1427,1429,1431,1434],{"class":262,"line":263},[260,1413,1414],{"class":293},"medium ",[260,1416,360],{"class":289},[260,1418,548],{"class":293},[260,1420,552],{"class":551},[260,1422,360],{"class":289},[260,1424,642],{"class":270},[260,1426,406],{"class":293},[260,1428,562],{"class":551},[260,1430,360],{"class":289},[260,1432,1433],{"class":270},"\"1E293B\"",[260,1435,570],{"class":293},[260,1437,1438,1441,1443,1445,1447,1449,1452,1454,1456,1458,1460],{"class":262,"line":303},[260,1439,1440],{"class":293},"double ",[260,1442,360],{"class":289},[260,1444,548],{"class":293},[260,1446,552],{"class":551},[260,1448,360],{"class":289},[260,1450,1451],{"class":270},"\"double\"",[260,1453,406],{"class":293},[260,1455,562],{"class":551},[260,1457,360],{"class":289},[260,1459,1433],{"class":270},[260,1461,570],{"class":293},[260,1463,1464],{"class":262,"line":310},[260,1465,307],{"emptyLinePlaceholder":306},[260,1467,1468,1471,1474],{"class":262,"line":323},[260,1469,1470],{"class":293},"grid(ws, ",[260,1472,1473],{"class":270},"\"A1:D5\"",[260,1475,1476],{"class":293},", thin)\n",[260,1478,1479,1481,1483,1485,1488,1490],{"class":262,"line":336},[260,1480,391],{"class":289},[260,1482,886],{"class":293},[260,1484,397],{"class":289},[260,1486,1487],{"class":293}," ws[",[260,1489,103],{"class":430},[260,1491,1492],{"class":293},"]:\n",[260,1494,1495,1498,1500,1502],{"class":262,"line":349},[260,1496,1497],{"class":293},"    set_edges(cell, ",[260,1499,611],{"class":551},[260,1501,360],{"class":289},[260,1503,1504],{"class":293},"medium)\n",[260,1506,1507,1509,1511,1513,1515,1517],{"class":262,"line":354},[260,1508,391],{"class":289},[260,1510,886],{"class":293},[260,1512,397],{"class":289},[260,1514,1487],{"class":293},[260,1516,216],{"class":430},[260,1518,1492],{"class":293},[260,1520,1521,1523,1525,1527],{"class":262,"line":366},[260,1522,1497],{"class":293},[260,1524,604],{"class":551},[260,1526,360],{"class":289},[260,1528,1529],{"class":293},"double)\n",[260,1531,1532,1535,1537],{"class":262,"line":377},[260,1533,1534],{"class":293},"outline(ws, ",[260,1536,1473],{"class":270},[260,1538,1539],{"class":293},", medium)\n",[260,1541,1542,1545,1548],{"class":262,"line":388},[260,1543,1544],{"class":293},"wb.save(",[260,1546,1547],{"class":270},"\"bordered.xlsx\"",[260,1549,570],{"class":293},[260,1551,1552,1554,1556,1558,1561,1564,1566,1569],{"class":262,"line":445},[260,1553,650],{"class":430},[260,1555,659],{"class":293},[260,1557,578],{"class":270},[260,1559,1560],{"class":293},"].border.left.style, ws[",[260,1562,1563],{"class":270},"\"B3\"",[260,1565,1560],{"class":293},[260,1567,1568],{"class":270},"\"B5\"",[260,1570,1571],{"class":293},"].border.top.style)\n",[260,1573,1574],{"class":262,"line":487},[260,1575,1576],{"class":683},"# medium thin double\n",[10,1578,1579],{},"Rendered, the table has a pale grid, a dark rule under the header, a double rule above the totals and a dark frame around everything. The double line above a total row is the accounting convention for \"this is the sum of the figures above\", and readers of financial reports look for it.",[246,1581,1583],{"id":1582},"step-4-bold-headers-without-losing-the-font","Step 4 — Bold headers without losing the font",[10,1585,1586,1587,1590],{},"The same immutability applies to fonts, with a subtler trap: ",[14,1588,1589],{},"Font(bold=True)"," creates a whole new font with no name or size, so the cell falls back to the application default — in LibreOffice's rendering, a serif face. Copy the existing font and change one attribute instead:",[251,1592,1594],{"className":280,"code":1593,"language":282,"meta":256,"style":256},"def make_bold(cell):\n    font = copy(cell.font)\n    font.bold = True\n    cell.font = font\n\nfor cell in list(ws[1]) + list(ws[5]):\n    make_bold(cell)\nwb.save(\"bordered.xlsx\")\nprint(ws[\"A1\"].font.name, ws[\"A1\"].font.sz, ws[\"A1\"].font.b)       # Calibri 11.0 True\n",[14,1595,1596,1606,1616,1626,1636,1640,1670,1675,1683],{"__ignoreMap":256},[260,1597,1598,1600,1603],{"class":262,"line":263},[260,1599,759],{"class":289},[260,1601,1602],{"class":762}," make_bold",[260,1604,1605],{"class":293},"(cell):\n",[260,1607,1608,1611,1613],{"class":262,"line":303},[260,1609,1610],{"class":293},"    font ",[260,1612,360],{"class":289},[260,1614,1615],{"class":293}," copy(cell.font)\n",[260,1617,1618,1621,1623],{"class":262,"line":310},[260,1619,1620],{"class":293},"    font.bold ",[260,1622,360],{"class":289},[260,1624,1625],{"class":430}," True\n",[260,1627,1628,1631,1633],{"class":262,"line":323},[260,1629,1630],{"class":293},"    cell.font ",[260,1632,360],{"class":289},[260,1634,1635],{"class":293}," font\n",[260,1637,1638],{"class":262,"line":336},[260,1639,307],{"emptyLinePlaceholder":306},[260,1641,1642,1644,1646,1648,1651,1653,1655,1658,1661,1663,1665,1667],{"class":262,"line":349},[260,1643,391],{"class":289},[260,1645,886],{"class":293},[260,1647,397],{"class":289},[260,1649,1650],{"class":430}," list",[260,1652,659],{"class":293},[260,1654,103],{"class":430},[260,1656,1657],{"class":293},"]) ",[260,1659,1660],{"class":289},"+",[260,1662,1650],{"class":430},[260,1664,659],{"class":293},[260,1666,216],{"class":430},[260,1668,1669],{"class":293},"]):\n",[260,1671,1672],{"class":262,"line":354},[260,1673,1674],{"class":293},"    make_bold(cell)\n",[260,1676,1677,1679,1681],{"class":262,"line":366},[260,1678,1544],{"class":293},[260,1680,1547],{"class":270},[260,1682,570],{"class":293},[260,1684,1685,1687,1689,1691,1694,1696,1699,1701,1704],{"class":262,"line":377},[260,1686,650],{"class":430},[260,1688,659],{"class":293},[260,1690,578],{"class":270},[260,1692,1693],{"class":293},"].font.name, ws[",[260,1695,578],{"class":270},[260,1697,1698],{"class":293},"].font.sz, ws[",[260,1700,578],{"class":270},[260,1702,1703],{"class":293},"].font.b)       ",[260,1705,1706],{"class":683},"# Calibri 11.0 True\n",[10,1708,1709,1712,1713,1226],{},[14,1710,1711],{},"copy"," produces an editable duplicate of the style object; after changing it, assign it back. The same pattern works for fills, alignment and protection. For a report with many styled ranges, a named style is more efficient than per-cell copies — see ",[19,1714,1716],{"href":1715},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fapply-a-reusable-style-theme-across-an-excel-report\u002F","Apply a Reusable Style Theme Across an Excel Report",[25,1718,34,1723,34,1726,34,1729,34,1732,34,1735,34,1740,34,1746,34,1750,34,1755,34,1757,34,1762,34,1765,34,1769,34,1771,34,1775,34,1777,34,1781,34,1783,34,1786,34,1789,34,1792,34,1794,34,1798,34,1801,34,1805,34,1807],{"viewBox":1719,"role":28,"ariaLabelledBy":1720,"xmlns":32,"style":1133},"4 0 752 242",[1721,1722],"bd-styles-t","bd-styles-d",[36,1724,1725],{"id":1721},"Border styles and where they fit",[40,1727,1728],{"id":1722},"Hair and thin lines suit inner grids. Medium lines mark the header rule and the table outline. Thick lines are for emphasis on a single edge. Double lines mark totals. Dashed and dotted lines suit input areas or annotations rather than data tables.",[44,1730],{"x":189,"y":47,"width":1142,"height":1731,"fill":50},"242",[44,1733],{"x":1146,"y":54,"width":1734,"height":97,"rx":47,"fill":50,"stroke":58,"style":59},"125.0",[44,1736],{"x":1737,"y":54,"width":1738,"height":97,"rx":47,"fill":106,"stroke":107,"style":1739},"145.0","595.0","stroke-width:1.2px",[65,1741,1745],{"x":1742,"y":1743,"style":1744},"442.5","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Typical use",[44,1747],{"x":1146,"y":1748,"width":1734,"height":1749,"rx":47,"fill":57,"stroke":58,"style":59},"56.0","34.0",[65,1751,1754],{"x":55,"y":1752,"style":1753},"78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","hair, thin",[44,1756],{"x":1737,"y":1748,"width":1738,"height":1749,"rx":47,"fill":50,"stroke":58,"style":59},[65,1758,1761],{"x":1742,"y":1759,"style":1760},"77.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","inner grid lines",[44,1763],{"x":1146,"y":1764,"width":1734,"height":1749,"rx":47,"fill":57,"stroke":58,"style":59},"90.0",[65,1766,1768],{"x":55,"y":1767,"style":1753},"112.0","medium",[44,1770],{"x":1737,"y":1764,"width":1738,"height":1749,"rx":47,"fill":50,"stroke":58,"style":59},[65,1772,1774],{"x":1742,"y":1773,"style":1760},"111.0","header rule, outline",[44,1776],{"x":1146,"y":185,"width":1734,"height":1749,"rx":47,"fill":57,"stroke":58,"style":59},[65,1778,1780],{"x":55,"y":1779,"style":1753},"146.0","thick",[44,1782],{"x":1737,"y":185,"width":1738,"height":1749,"rx":47,"fill":50,"stroke":58,"style":59},[65,1784,1785],{"x":1742,"y":1737,"style":1760},"one strong edge",[44,1787],{"x":1146,"y":1788,"width":1734,"height":1749,"rx":47,"fill":57,"stroke":58,"style":59},"158.0",[65,1790,1791],{"x":55,"y":1353,"style":1753},"double",[44,1793],{"x":1737,"y":1788,"width":1738,"height":1749,"rx":47,"fill":50,"stroke":58,"style":59},[65,1795,1797],{"x":1742,"y":1796,"style":1760},"179.0","above a total row",[44,1799],{"x":1146,"y":1800,"width":1734,"height":1749,"rx":47,"fill":57,"stroke":58,"style":59},"192.0",[65,1802,1804],{"x":55,"y":1803,"style":1753},"214.0","dashed, dotted",[44,1806],{"x":1737,"y":1800,"width":1738,"height":1749,"rx":47,"fill":50,"stroke":58,"style":59},[65,1808,1810],{"x":1742,"y":1809,"style":1760},"213.0","input areas and notes",[246,1812,1814],{"id":1813},"step-5-merged-cells","Step 5 — Merged cells",[10,1816,1817],{},"A merged range is drawn from its individual cells, so an outline around a merged title needs the edges on every cell in the block — setting a border on the top-left cell alone leaves the rest of the frame missing:",[251,1819,1821],{"className":280,"code":1820,"language":282,"meta":256,"style":256},"ws.insert_rows(1)\nws[\"A1\"] = \"Quarterly sales\"\nws.merge_cells(\"A1:D1\")\noutline(ws, \"A1:D1\", medium)\nwb.save(\"bordered.xlsx\")\nprint([ws.cell(1, c).border.top.style for c in range(1, 5)])      # ['medium', 'medium', 'medium', 'medium']\n",[14,1822,1823,1832,1845,1855,1863,1871],{"__ignoreMap":256},[260,1824,1825,1828,1830],{"class":262,"line":263},[260,1826,1827],{"class":293},"ws.insert_rows(",[260,1829,103],{"class":430},[260,1831,570],{"class":293},[260,1833,1834,1836,1838,1840,1842],{"class":262,"line":303},[260,1835,575],{"class":293},[260,1837,578],{"class":270},[260,1839,1032],{"class":293},[260,1841,360],{"class":289},[260,1843,1844],{"class":270}," \"Quarterly sales\"\n",[260,1846,1847,1850,1853],{"class":262,"line":310},[260,1848,1849],{"class":293},"ws.merge_cells(",[260,1851,1852],{"class":270},"\"A1:D1\"",[260,1854,570],{"class":293},[260,1856,1857,1859,1861],{"class":262,"line":323},[260,1858,1534],{"class":293},[260,1860,1852],{"class":270},[260,1862,1539],{"class":293},[260,1864,1865,1867,1869],{"class":262,"line":336},[260,1866,1544],{"class":293},[260,1868,1547],{"class":270},[260,1870,570],{"class":293},[260,1872,1873,1875,1878,1880,1883,1885,1888,1890,1893,1895,1897,1899,1901,1904],{"class":262,"line":349},[260,1874,650],{"class":430},[260,1876,1877],{"class":293},"([ws.cell(",[260,1879,103],{"class":430},[260,1881,1882],{"class":293},", c).border.top.style ",[260,1884,391],{"class":289},[260,1886,1887],{"class":293}," c ",[260,1889,397],{"class":289},[260,1891,1892],{"class":430}," range",[260,1894,653],{"class":293},[260,1896,103],{"class":430},[260,1898,406],{"class":293},[260,1900,216],{"class":430},[260,1902,1903],{"class":293},")])      ",[260,1905,1906],{"class":683},"# ['medium', 'medium', 'medium', 'medium']\n",[10,1908,1909,1911,1912,1226],{},[14,1910,1122],{}," already walks every cell of the range, so it handles merged blocks without special cases. Inserting a row shifts the cells and their borders down with them, so the table keeps its formatting below the new title. Merging itself is covered in ",[19,1913,1915],{"href":1914},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fmerge-cells-and-centre-a-report-title-with-openpyxl\u002F","Merge Cells and Centre a Report Title with openpyxl",[246,1917,1919],{"id":1918},"step-6-border-a-table-written-by-pandas","Step 6 — Border a table written by pandas",[10,1921,1922,1923,1926],{},"In most reports the table comes from ",[14,1924,1925],{},"DataFrame.to_excel",", and the borders go on afterwards. The range follows from the frame's shape, so compute it instead of hard-coding it — the report may have four rows today and forty next month:",[251,1928,1930],{"className":280,"code":1929,"language":282,"meta":256,"style":256},"import pandas as pd\nfrom openpyxl.utils import get_column_letter\n\nreport = pd.DataFrame({\"Region\": [\"North\", \"South\", \"East\"], \"Q1\": [120, 80, 40], \"Q2\": [140, 90, 55]})\nwith pd.ExcelWriter(\"pandas_bordered.xlsx\", engine=\"openpyxl\") as writer:\n    report.to_excel(writer, sheet_name=\"Report\", index=False, startrow=1)\n    sheet = writer.sheets[\"Report\"]\n    first_row = 2                                            # header row after startrow=1\n    last_row = first_row + len(report)\n    last_col = get_column_letter(len(report.columns))\n    table_ref = f\"A{first_row}:{last_col}{last_row}\"\n    grid(sheet, table_ref, thin)\n    for cell in sheet[first_row]:\n        set_edges(cell, bottom=medium)\n    outline(sheet, table_ref, medium)\nprint(table_ref)                                             # A2:C5\n",[14,1931,1932,1945,1957,1961,2022,2051,2085,2100,2113,2131,2147,2188,2193,2204,2215,2220],{"__ignoreMap":256},[260,1933,1934,1936,1939,1942],{"class":262,"line":263},[260,1935,297],{"class":289},[260,1937,1938],{"class":293}," pandas ",[260,1940,1941],{"class":289},"as",[260,1943,1944],{"class":293}," pd\n",[260,1946,1947,1949,1952,1954],{"class":262,"line":303},[260,1948,290],{"class":289},[260,1950,1951],{"class":293}," openpyxl.utils ",[260,1953,297],{"class":289},[260,1955,1956],{"class":293}," get_column_letter\n",[260,1958,1959],{"class":262,"line":310},[260,1960,307],{"emptyLinePlaceholder":306},[260,1962,1963,1966,1968,1971,1973,1976,1978,1980,1982,1984,1986,1989,1991,1993,1995,1997,1999,2001,2003,2005,2007,2009,2011,2013,2015,2017,2019],{"class":262,"line":323},[260,1964,1965],{"class":293},"report ",[260,1967,360],{"class":289},[260,1969,1970],{"class":293}," pd.DataFrame({",[260,1972,403],{"class":270},[260,1974,1975],{"class":293},": [",[260,1977,425],{"class":270},[260,1979,406],{"class":293},[260,1981,451],{"class":270},[260,1983,406],{"class":293},[260,1985,469],{"class":270},[260,1987,1988],{"class":293},"], ",[260,1990,409],{"class":270},[260,1992,1975],{"class":293},[260,1994,145],{"class":430},[260,1996,406],{"class":293},[260,1998,172],{"class":430},[260,2000,406],{"class":293},[260,2002,199],{"class":430},[260,2004,1988],{"class":293},[260,2006,414],{"class":270},[260,2008,1975],{"class":293},[260,2010,150],{"class":430},[260,2012,406],{"class":293},[260,2014,177],{"class":430},[260,2016,406],{"class":293},[260,2018,204],{"class":430},[260,2020,2021],{"class":293},"]})\n",[260,2023,2024,2027,2030,2033,2035,2038,2040,2043,2046,2048],{"class":262,"line":336},[260,2025,2026],{"class":289},"with",[260,2028,2029],{"class":293}," pd.ExcelWriter(",[260,2031,2032],{"class":270},"\"pandas_bordered.xlsx\"",[260,2034,406],{"class":293},[260,2036,2037],{"class":551},"engine",[260,2039,360],{"class":289},[260,2041,2042],{"class":270},"\"openpyxl\"",[260,2044,2045],{"class":293},") ",[260,2047,1941],{"class":289},[260,2049,2050],{"class":293}," writer:\n",[260,2052,2053,2056,2059,2061,2064,2066,2069,2071,2074,2076,2079,2081,2083],{"class":262,"line":349},[260,2054,2055],{"class":293},"    report.to_excel(writer, ",[260,2057,2058],{"class":551},"sheet_name",[260,2060,360],{"class":289},[260,2062,2063],{"class":270},"\"Report\"",[260,2065,406],{"class":293},[260,2067,2068],{"class":551},"index",[260,2070,360],{"class":289},[260,2072,2073],{"class":430},"False",[260,2075,406],{"class":293},[260,2077,2078],{"class":551},"startrow",[260,2080,360],{"class":289},[260,2082,103],{"class":430},[260,2084,570],{"class":293},[260,2086,2087,2090,2092,2095,2097],{"class":262,"line":354},[260,2088,2089],{"class":293},"    sheet ",[260,2091,360],{"class":289},[260,2093,2094],{"class":293}," writer.sheets[",[260,2096,2063],{"class":270},[260,2098,2099],{"class":293},"]\n",[260,2101,2102,2105,2107,2110],{"class":262,"line":366},[260,2103,2104],{"class":293},"    first_row ",[260,2106,360],{"class":289},[260,2108,2109],{"class":430}," 2",[260,2111,2112],{"class":683},"                                            # header row after startrow=1\n",[260,2114,2115,2118,2120,2123,2125,2128],{"class":262,"line":377},[260,2116,2117],{"class":293},"    last_row ",[260,2119,360],{"class":289},[260,2121,2122],{"class":293}," first_row ",[260,2124,1660],{"class":289},[260,2126,2127],{"class":430}," len",[260,2129,2130],{"class":293},"(report)\n",[260,2132,2133,2136,2138,2141,2144],{"class":262,"line":388},[260,2134,2135],{"class":293},"    last_col ",[260,2137,360],{"class":289},[260,2139,2140],{"class":293}," get_column_letter(",[260,2142,2143],{"class":430},"len",[260,2145,2146],{"class":293},"(report.columns))\n",[260,2148,2149,2152,2154,2157,2160,2164,2167,2170,2172,2174,2177,2180,2183,2185],{"class":262,"line":445},[260,2150,2151],{"class":293},"    table_ref ",[260,2153,360],{"class":289},[260,2155,2156],{"class":289}," f",[260,2158,2159],{"class":270},"\"A",[260,2161,2163],{"class":2162},"sSjpA","{",[260,2165,2166],{"class":293},"first_row",[260,2168,2169],{"class":2162},"}",[260,2171,1193],{"class":270},[260,2173,2163],{"class":2162},[260,2175,2176],{"class":293},"last_col",[260,2178,2179],{"class":2162},"}{",[260,2181,2182],{"class":293},"last_row",[260,2184,2169],{"class":2162},[260,2186,2187],{"class":270},"\"\n",[260,2189,2190],{"class":262,"line":487},[260,2191,2192],{"class":293},"    grid(sheet, table_ref, thin)\n",[260,2194,2195,2197,2199,2201],{"class":262,"line":512},[260,2196,871],{"class":289},[260,2198,886],{"class":293},[260,2200,397],{"class":289},[260,2202,2203],{"class":293}," sheet[first_row]:\n",[260,2205,2206,2209,2211,2213],{"class":262,"line":938},[260,2207,2208],{"class":293},"        set_edges(cell, ",[260,2210,611],{"class":551},[260,2212,360],{"class":289},[260,2214,1504],{"class":293},[260,2216,2217],{"class":262,"line":949},[260,2218,2219],{"class":293},"    outline(sheet, table_ref, medium)\n",[260,2221,2222,2224,2227],{"class":262,"line":993},[260,2223,650],{"class":430},[260,2225,2226],{"class":293},"(table_ref)                                             ",[260,2228,2229],{"class":683},"# A2:C5\n",[10,2231,2232,2234,2235,2238,2239,2242],{},[14,2233,2078],{}," is zero-based in pandas and leaves row 1 free for a title, so the header lands in row 2. The current pandas release writes a plain header — no bold, no border — so the grid and the header rule supply all the structure; make the header bold with the ",[14,2236,2237],{},"make_bold"," helper from Step 4. Older pandas versions styled the header themselves, bold with thin borders, and the helpers simply layer on top of that. Writing the table and styling it in the same ",[14,2240,2241],{},"ExcelWriter"," block means the file is saved once, with borders included.",[246,2244,2246],{"id":2245},"common-pitfalls","Common pitfalls",[2248,2249,2250,2258,2268,2277],"ul",{},[2251,2252,2253,2257],"li",{},[2254,2255,2256],"strong",{},"Borders on the neighbour."," The line between A1 and A2 can come from A1's bottom or A2's top. Pick one convention — this guide always sets both through the grid — or lines appear twice as thick in some viewers.",[2251,2259,2260,2263,2264,2267],{},[2254,2261,2262],{},"Borders that do not print."," Very light colours such as ",[14,2265,2266],{},"E7EBEF"," look fine on screen and vanish on paper. Check a print preview, or use a slightly darker grey for grids in reports that are printed.",[2251,2269,2270,2273,2274,2276],{},[2254,2271,2272],{},"Styling huge ranges cell by cell."," Hundreds of thousands of ",[14,2275,748],{}," calls are slow. Style the header and totals explicitly and rely on a table style or banding for the body.",[2251,2278,2279,1167,2282,2285],{},[2254,2280,2281],{},"Colours with a #.",[14,2283,2284],{},"Side(color=\"#CDD5E6\")"," raises \"Colors must be aRGB hex values\"; pass the six hex digits without the hash, or an 8-digit ARGB value.",[246,2287,2289],{"id":2288},"conclusion","Conclusion",[10,2291,2292,2293,406,2295,2298,2299,2301],{},"openpyxl borders are per cell and replaced as a whole, so build them through a helper that copies the current border and swaps only the named edges. With ",[14,2294,748],{},[14,2296,2297],{},"grid"," and ",[14,2300,1122],{}," you can layer a light grid, a medium header rule, a double total rule and an outer frame in any order, border merged blocks cell by cell, and — by the same copy-then-assign pattern — make text bold without losing its font.",[246,2303,2305],{"id":2304},"frequently-asked-questions","Frequently asked questions",[10,2307,2308,2311],{},[2254,2309,2310],{},"How do I put a border around a range with openpyxl?","\nopenpyxl has no range-level border, so loop over the cells and set only the outer edges — top on the first row, bottom on the last, left on the first column and right on the last — while keeping each cell's other edges.",[10,2313,2314,2317],{},[2254,2315,2316],{},"Why did my border disappear when I added another one?","\nAssigning cell.border replaces all four edges. Setting a bottom border after a grid removes the grid's left, right and top lines. Build the new Border from the existing one, changing only the edges you want.",[10,2319,2320,2323],{},[2254,2321,2322],{},"Why do I get \"Style objects are immutable\"?","\nopenpyxl style objects cannot be changed in place, so cell.border.left = Side(...) fails. Create a new Border — or copy the existing one with the changed edge — and assign it to cell.border.",[10,2325,2326,2329],{},[2254,2327,2328],{},"How do I border merged cells?","\nSet the border on every cell covered by the merge, not just the top-left one. Excel draws each edge from the cell that sits on it, so the outer cells of the merged block need the outline edges.",[246,2331,2333],{"id":2332},"related","Related",[2248,2335,2336,2342,2349,2356],{},[2251,2337,2338,2339,2341],{},"Up one level: ",[19,2340,22],{"href":21}," — fonts, fills, borders and layout.",[2251,2343,2344,2348],{},[19,2345,2347],{"href":2346},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-alternating-row-colors-with-openpyxl\u002F","Add Alternating Row Colors with openpyxl"," — banding as an alternative to a grid.",[2251,2350,2351,2355],{},[19,2352,2354],{"href":2353},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fwrap-text-and-align-cells-with-openpyxl\u002F","Wrap Text and Align Cells with openpyxl"," — the other half of a tidy table.",[2251,2357,2358,2362],{},[19,2359,2361],{"href":2360},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fcopy-cell-styles-with-openpyxl\u002F","Copy Cell Styles from One Cell to Another with openpyxl"," — reusing a styled cell's look.",[552,2364,2365],{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":256,"searchDepth":303,"depth":303,"links":2367},[2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378],{"id":248,"depth":303,"text":249},{"id":518,"depth":303,"text":519},{"id":742,"depth":303,"text":743},{"id":1400,"depth":303,"text":1401},{"id":1582,"depth":303,"text":1583},{"id":1813,"depth":303,"text":1814},{"id":1918,"depth":303,"text":1919},{"id":2245,"depth":303,"text":2246},{"id":2288,"depth":303,"text":2289},{"id":2304,"depth":303,"text":2305},{"id":2332,"depth":303,"text":2333},"2026-09-23","openpyxl sets borders per cell, and every assignment replaces the whole border. Build helpers that draw a light grid, an outline around a table, a heavy line under the header and a double line above totals — without wiping edges set earlier — and handle merged cells and immutable style objects correctly.","md",[2383,2385,2387,2389],{"q":2310,"a":2384},"openpyxl has no range-level border, so loop over the cells and set only the outer edges — top on the first row, bottom on the last, left on the first column and right on the last — while keeping each cell's other edges.",{"q":2316,"a":2386},"Assigning cell.border replaces all four edges. Setting a bottom border after a grid removes the grid's left, right and top lines. Build the new Border from the existing one, changing only the edges you want.",{"q":2322,"a":2388},"openpyxl style objects cannot be changed in place, so cell.border.left = Side(...) fails. Create a new Border — or copy the existing one with the changed edge — and assign it to cell.border.",{"q":2328,"a":2390},"Set the border on every cell covered by the merge, not just the top-left one. Excel draws each edge from the cell that sits on it, so the outer cells of the merged block need the outline edges.","add borders to a range with openpyxl",null,{"breadcrumb":2394},[2395,2398,2401],{"name":2396,"item":2397},"Home","\u002F",{"name":2399,"item":2400},"Formatting and Charting Excel Reports with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002F",{"name":22,"item":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-borders-to-a-range-with-openpyxl",{"title":2404,"description":2405},"Add Borders to a Range with openpyxl (outline, grid, totals)","Border a range in Excel with openpyxl — Border and Side objects, an outline helper that keeps existing edges, inner grid lines, medium header rule, double line above totals, border styles and colours, merged cells and the immutable style error.","add-borders-to-a-range-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-borders-to-a-range-with-openpyxl\u002Findex",[2409,2410],"cell-styling","openpyxl","how-to","M2NC8trSoLtyvD0MW2Fu-7LvsPeZOvqGsVrTEhnsZD4",[2414,2417],{"title":2347,"path":2415,"stem":2416,"children":-1},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-alternating-row-colors-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-alternating-row-colors-with-openpyxl\u002Findex",{"title":2418,"path":2419,"stem":2420,"children":-1},"Add Hyperlinks to Excel Cells with Python","\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-hyperlinks-to-excel-cells-with-python","formatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl\u002Fadd-hyperlinks-to-excel-cells-with-python\u002Findex",[2422,2424],{"path":2423,"title":2399},"\u002Fformatting-and-charting-excel-reports-with-python",{"path":2425,"title":22},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fstyling-excel-cells-with-openpyxl",[],[2428,2431,2433,2436,2439,2442],{"path":2429,"title":2430,"kind":2411},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-rich-text-in-a-cell-with-xlsxwriter","Write Rich Text with Mixed Formats in One Cell with xlsxwriter",{"path":2432,"title":2361,"kind":2411},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fcopy-cell-styles-with-openpyxl",{"path":2434,"title":2435,"kind":2411},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fauto-fit-column-widths-when-writing-with-pandas","Auto-Fit Column Widths When Writing with pandas",{"path":2437,"title":2438,"kind":2411},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fexport-a-styled-pandas-dataframe-to-excel","Export a Styled pandas DataFrame to Excel",{"path":2440,"title":2441,"kind":2411},"\u002Fautomating-reporting-workflows\u002Fbuilding-finance-and-operations-reports\u002Fgenerate-a-staff-rota-in-excel-with-python","Generate a Staff Rota in Excel with Python",{"path":2443,"title":2444,"kind":2411},"\u002Fautomating-reporting-workflows\u002Fbuilding-multi-sheet-excel-dashboards\u002Fadd-a-table-of-contents-sheet-with-hyperlinks-in-excel","Add a Table of Contents Sheet with Hyperlinks in Excel",1790972117102]