[{"data":1,"prerenderedAt":2301},["ShallowReactive",2],{"doc:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-calculated-columns-to-an-excel-table-with-python":3,"surround:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-calculated-columns-to-an-excel-table-with-python":2270,"ancestors:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-calculated-columns-to-an-excel-table-with-python":2279,"children:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-calculated-columns-to-an-excel-table-with-python":2284,"related:\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-calculated-columns-to-an-excel-table-with-python":2285},{"id":4,"title":5,"body":6,"dateModified":2235,"datePublished":2235,"description":2236,"extension":2237,"faq":2238,"keyword":2248,"kind":2249,"meta":2250,"navigation":118,"path":2259,"seo":2260,"slug":2263,"stem":2264,"topics":2265,"type":2268,"__hash__":2269},"docs\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-calculated-columns-to-an-excel-table-with-python\u002Findex.md","Add Calculated Columns to an Excel Table with Python",{"type":7,"value":8,"toc":2224},"minimark",[9,24,29,58,73,77,319,333,337,535,561,753,757,760,879,917,1160,1164,1167,1565,1568,1970,1973,1985,2081,2085,2132,2136,2152,2156,2162,2168,2174,2187,2191,2220],[10,11,12,13,17,18,23],"p",{},"Workbooks that arrive from other people or systems often already contain an Excel table — an order log, a price list, an export someone formatted as a table. Adding a column of figures derived from the others is a frequent job: a margin from price and cost, an age from a date, a flag from a status. Writing values into the cells next to the table looks fine until someone adds a row in Excel and the new row has no value, or filters the table and the extra column is not part of it. The proper result is a ",[14,15,16],"em",{},"calculated column",": part of the table, with a formula Excel fills into every row — including rows typed in later. openpyxl can do that, but it does not do it for you: the table's range, its autofilter, its column list and the formulas in the cells all have to be updated together. This page does each step on an existing table, adds a total, and checks the formulas by recalculating. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002F","Creating Excel Tables and Autofilters with Python",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install openpyxl pandas\n","bash","",[37,38,39],"code",{"__ignoreMap":35},[40,41,44,48,52,55],"span",{"class":42,"line":43},"line",1,[40,45,47],{"class":46},"sMTad","pip",[40,49,51],{"class":50},"srMev"," install",[40,53,54],{"class":50}," openpyxl",[40,56,57],{"class":50}," pandas\n",[10,59,60,61,64,65,68,69,72],{},"The starting point is ",[37,62,63],{},"products.xlsx",", a workbook with a table named ",[14,66,67],{},"Products"," covering A1:E6 — SKU, product, price, unit cost and units sold for five products — and a note someone typed in G2. LibreOffice (",[37,70,71],{},"soffice",") recalculates the result in the last step.",[25,74,76],{"id":75},"step-1-find-the-table-and-check-there-is-room","Step 1 — Find the table and check there is room",[30,78,82],{"className":79,"code":80,"language":81,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","from openpyxl import load_workbook\nfrom openpyxl.utils import get_column_letter, range_boundaries\n\nwb = load_workbook(\"products.xlsx\")\nws = wb[\"Products\"]\ntable = ws.tables[\"Products\"]\nmin_col, min_row, max_col, max_row = range_boundaries(table.ref)\nnew_col = max_col + 1\nletter = get_column_letter(new_col)\noccupied = [ws.cell(r, new_col).coordinate for r in range(min_row, max_row + 1) if ws.cell(r, new_col).value is not None]\nprint(\"table\", table.displayName, table.ref, \"| new column\", letter, \"| occupied cells:\", occupied or \"none\")\nprint(\"existing columns:\", [c.name for c in table.tableColumns])\n","python",[37,83,84,100,113,120,138,155,170,181,199,210,261,296],{"__ignoreMap":35},[40,85,86,90,94,97],{"class":42,"line":43},[40,87,89],{"class":88},"s-kum","from",[40,91,93],{"class":92},"skGVy"," openpyxl ",[40,95,96],{"class":88},"import",[40,98,99],{"class":92}," load_workbook\n",[40,101,103,105,108,110],{"class":42,"line":102},2,[40,104,89],{"class":88},[40,106,107],{"class":92}," openpyxl.utils ",[40,109,96],{"class":88},[40,111,112],{"class":92}," get_column_letter, range_boundaries\n",[40,114,116],{"class":42,"line":115},3,[40,117,119],{"emptyLinePlaceholder":118},true,"\n",[40,121,123,126,129,132,135],{"class":42,"line":122},4,[40,124,125],{"class":92},"wb ",[40,127,128],{"class":88},"=",[40,130,131],{"class":92}," load_workbook(",[40,133,134],{"class":50},"\"products.xlsx\"",[40,136,137],{"class":92},")\n",[40,139,141,144,146,149,152],{"class":42,"line":140},5,[40,142,143],{"class":92},"ws ",[40,145,128],{"class":88},[40,147,148],{"class":92}," wb[",[40,150,151],{"class":50},"\"Products\"",[40,153,154],{"class":92},"]\n",[40,156,158,161,163,166,168],{"class":42,"line":157},6,[40,159,160],{"class":92},"table ",[40,162,128],{"class":88},[40,164,165],{"class":92}," ws.tables[",[40,167,151],{"class":50},[40,169,154],{"class":92},[40,171,173,176,178],{"class":42,"line":172},7,[40,174,175],{"class":92},"min_col, min_row, max_col, max_row ",[40,177,128],{"class":88},[40,179,180],{"class":92}," range_boundaries(table.ref)\n",[40,182,184,187,189,192,195],{"class":42,"line":183},8,[40,185,186],{"class":92},"new_col ",[40,188,128],{"class":88},[40,190,191],{"class":92}," max_col ",[40,193,194],{"class":88},"+",[40,196,198],{"class":197},"sP0c6"," 1\n",[40,200,202,205,207],{"class":42,"line":201},9,[40,203,204],{"class":92},"letter ",[40,206,128],{"class":88},[40,208,209],{"class":92}," get_column_letter(new_col)\n",[40,211,213,216,218,221,224,227,230,233,236,238,241,244,247,250,253,256,259],{"class":42,"line":212},10,[40,214,215],{"class":92},"occupied ",[40,217,128],{"class":88},[40,219,220],{"class":92}," [ws.cell(r, new_col).coordinate ",[40,222,223],{"class":88},"for",[40,225,226],{"class":92}," r ",[40,228,229],{"class":88},"in",[40,231,232],{"class":197}," range",[40,234,235],{"class":92},"(min_row, max_row ",[40,237,194],{"class":88},[40,239,240],{"class":197}," 1",[40,242,243],{"class":92},") ",[40,245,246],{"class":88},"if",[40,248,249],{"class":92}," ws.cell(r, new_col).value ",[40,251,252],{"class":88},"is",[40,254,255],{"class":88}," not",[40,257,258],{"class":197}," None",[40,260,154],{"class":92},[40,262,264,267,270,273,276,279,282,285,288,291,294],{"class":42,"line":263},11,[40,265,266],{"class":197},"print",[40,268,269],{"class":92},"(",[40,271,272],{"class":50},"\"table\"",[40,274,275],{"class":92},", table.displayName, table.ref, ",[40,277,278],{"class":50},"\"| new column\"",[40,280,281],{"class":92},", letter, ",[40,283,284],{"class":50},"\"| occupied cells:\"",[40,286,287],{"class":92},", occupied ",[40,289,290],{"class":88},"or",[40,292,293],{"class":50}," \"none\"",[40,295,137],{"class":92},[40,297,299,301,303,306,309,311,314,316],{"class":42,"line":298},12,[40,300,266],{"class":197},[40,302,269],{"class":92},[40,304,305],{"class":50},"\"existing columns:\"",[40,307,308],{"class":92},", [c.name ",[40,310,223],{"class":88},[40,312,313],{"class":92}," c ",[40,315,229],{"class":88},[40,317,318],{"class":92}," table.tableColumns])\n",[10,320,321,324,325,328,329,332],{},[37,322,323],{},"ws.tables"," maps table names to their definitions; ",[37,326,327],{},"range_boundaries"," turns the reference into column and row numbers. The new column goes immediately to the right, in F. Checking that those cells are empty matters: a table cannot overlap other content, and silently writing over someone's notes is worse than stopping. Here column F is free — the note is in G — so the table can grow. A table's columns are also listed by name in ",[37,330,331],{},"tableColumns",", and that list must match the header cells exactly, which is why the next step adds to both.",[25,334,336],{"id":335},"step-2-extend-the-table-definition","Step 2 — Extend the table definition",[30,338,340],{"className":79,"code":339,"language":81,"meta":35,"style":35},"from openpyxl.worksheet.table import TableColumn, TableFormula\n\nNAME = \"Margin %\"\nFORMULA = \"IFERROR((Products[[#This Row],[Price]]-Products[[#This Row],[Unit cost]])\u002FProducts[[#This Row],[Price]],0)\"\n\nws.cell(min_row, new_col, NAME)\ncolumn = TableColumn(id=max(c.id for c in table.tableColumns) + 1, name=NAME)\ncolumn.calculatedColumnFormula = TableFormula(attr_text=FORMULA)\ntable.tableColumns.append(column)\ntable.ref = f\"{get_column_letter(min_col)}{min_row}:{letter}{max_row}\"\ntable.autoFilter.ref = table.ref\nprint(table.ref, [c.name for c in table.tableColumns])\n",[37,341,342,354,358,369,379,383,392,439,458,463,510,520],{"__ignoreMap":35},[40,343,344,346,349,351],{"class":42,"line":43},[40,345,89],{"class":88},[40,347,348],{"class":92}," openpyxl.worksheet.table ",[40,350,96],{"class":88},[40,352,353],{"class":92}," TableColumn, TableFormula\n",[40,355,356],{"class":42,"line":102},[40,357,119],{"emptyLinePlaceholder":118},[40,359,360,363,366],{"class":42,"line":115},[40,361,362],{"class":197},"NAME",[40,364,365],{"class":88}," =",[40,367,368],{"class":50}," \"Margin %\"\n",[40,370,371,374,376],{"class":42,"line":122},[40,372,373],{"class":197},"FORMULA",[40,375,365],{"class":88},[40,377,378],{"class":50}," \"IFERROR((Products[[#This Row],[Price]]-Products[[#This Row],[Unit cost]])\u002FProducts[[#This Row],[Price]],0)\"\n",[40,380,381],{"class":42,"line":140},[40,382,119],{"emptyLinePlaceholder":118},[40,384,385,388,390],{"class":42,"line":157},[40,386,387],{"class":92},"ws.cell(min_row, new_col, ",[40,389,362],{"class":197},[40,391,137],{"class":92},[40,393,394,397,399,402,406,408,411,414,416,418,420,423,425,427,430,433,435,437],{"class":42,"line":172},[40,395,396],{"class":92},"column ",[40,398,128],{"class":88},[40,400,401],{"class":92}," TableColumn(",[40,403,405],{"class":404},"sa561","id",[40,407,128],{"class":88},[40,409,410],{"class":197},"max",[40,412,413],{"class":92},"(c.id ",[40,415,223],{"class":88},[40,417,313],{"class":92},[40,419,229],{"class":88},[40,421,422],{"class":92}," table.tableColumns) ",[40,424,194],{"class":88},[40,426,240],{"class":197},[40,428,429],{"class":92},", ",[40,431,432],{"class":404},"name",[40,434,128],{"class":88},[40,436,362],{"class":197},[40,438,137],{"class":92},[40,440,441,444,446,449,452,454,456],{"class":42,"line":183},[40,442,443],{"class":92},"column.calculatedColumnFormula ",[40,445,128],{"class":88},[40,447,448],{"class":92}," TableFormula(",[40,450,451],{"class":404},"attr_text",[40,453,128],{"class":88},[40,455,373],{"class":197},[40,457,137],{"class":92},[40,459,460],{"class":42,"line":201},[40,461,462],{"class":92},"table.tableColumns.append(column)\n",[40,464,465,468,470,473,476,480,483,486,489,492,495,497,500,502,505,507],{"class":42,"line":212},[40,466,467],{"class":92},"table.ref ",[40,469,128],{"class":88},[40,471,472],{"class":88}," f",[40,474,475],{"class":50},"\"",[40,477,479],{"class":478},"sSjpA","{",[40,481,482],{"class":92},"get_column_letter(min_col)",[40,484,485],{"class":478},"}{",[40,487,488],{"class":92},"min_row",[40,490,491],{"class":478},"}",[40,493,494],{"class":50},":",[40,496,479],{"class":478},[40,498,499],{"class":92},"letter",[40,501,485],{"class":478},[40,503,504],{"class":92},"max_row",[40,506,491],{"class":478},[40,508,509],{"class":50},"\"\n",[40,511,512,515,517],{"class":42,"line":263},[40,513,514],{"class":92},"table.autoFilter.ref ",[40,516,128],{"class":88},[40,518,519],{"class":92}," table.ref\n",[40,521,522,524,527,529,531,533],{"class":42,"line":298},[40,523,266],{"class":197},[40,525,526],{"class":92},"(table.ref, [c.name ",[40,528,223],{"class":88},[40,530,313],{"class":92},[40,532,229],{"class":88},[40,534,318],{"class":92},[10,536,537,538,541,542,545,546,549,550,553,554,557,558,560],{},"Four things change together: the header cell, a new ",[37,539,540],{},"TableColumn"," with the next unused id and the same name as the header, its ",[37,543,544],{},"calculatedColumnFormula",", and the table's ",[37,547,548],{},"ref"," and ",[37,551,552],{},"autoFilter.ref",", which both widen to column F. Leaving any of them out produces a file Excel offers to repair on opening — usually by deleting the table. The calculated-column formula is what tells Excel this column is calculated: when someone adds a row to the table in Excel, Excel copies this formula into it. It is written ",[14,555,556],{},"without"," the leading ",[37,559,128],{},", as Excel stores it.",[562,563,571,572,571,576,571,580,571,588,571,598,571,609,571,616,571,621,571,627,571,632,571,636,571,643,571,649,571,651,571,654,571,658,571,662,571,665,571,670,571,677,571,682,571,685,571,688,571,691,571,695,571,698,571,703,571,710,571,716,571,720,571,724,571,728,571,731,571,734,571,740,571,744,571,747,571,750],"svg",{"viewBox":564,"role":565,"ariaLabelledBy":566,"xmlns":569,"style":570},"4 0 752 160","img",[567,568],"cc-steps-t","cc-steps-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[573,574,575],"title",{"id":567},"What changes when a calculated column is added",[577,578,579],"desc",{"id":568},"Check that the column to the right of the table is empty. Write the header, add a TableColumn with the next id, the same name and a calculated-column formula, widen the table reference and the autofilter, then write the formula into every data row.",[581,582],"rect",{"x":583,"y":584,"width":585,"height":586,"fill":587},"4","0","752","160","#ffffff",[581,589],{"x":590,"y":591,"width":592,"height":593,"rx":594,"fill":595,"stroke":596,"style":597},"20.0","16.0","115.2","128.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[599,600,602],"g",{"transform":601},"translate(65.6,46.5) scale(1.000)",[603,604],"path",{"d":605,"fill":606,"stroke":607,"style":608},"M11 4a7 7 0 1 0 .01 0z M16 16l5 5","none","var(--text,#172033)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[610,611,615],"text",{"x":612,"y":613,"style":614},"77.6","93.0","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Check space",[610,617,620],{"x":612,"y":618,"style":619},"112.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","column F empty",[42,622],{"x1":623,"y1":624,"x2":625,"y2":624,"stroke":626,"style":597},"140.2","80.0","162.2","var(--brand,#5b5cf0)",[628,629],"polygon",{"points":630,"fill":631},"166.2,80.0 157.2,85.0 157.2,75.0","#5b5cf0",[581,633],{"x":634,"y":591,"width":592,"height":593,"rx":594,"fill":635,"stroke":626,"style":597},"171.2","#f0f4ff",[599,637,639],{"transform":638},"translate(216.8,29.0) scale(1.000)",[603,640],{"d":641,"fill":606,"stroke":642,"style":608},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--brand-strong,#4338ca)",[610,644,648],{"x":645,"y":646,"style":647},"228.8","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Header +",[610,650,540],{"x":645,"y":613,"style":647},[610,652,653],{"x":645,"y":618,"style":619},"same name,",[610,655,657],{"x":645,"y":656,"style":619},"130.0","next id",[42,659],{"x1":660,"y1":624,"x2":661,"y2":624,"stroke":626,"style":597},"291.4","313.4",[628,663],{"points":664,"fill":631},"317.4,80.0 308.4,85.0 308.4,75.0",[581,666],{"x":667,"y":591,"width":592,"height":593,"rx":594,"fill":668,"stroke":669,"style":597},"322.4","#d9f4f1","var(--teal,#0f9488)",[599,671,673],{"transform":672},"translate(368.0,29.0) scale(1.000)",[603,674],{"d":675,"fill":606,"stroke":676,"style":608},"M9 8l-4 4 4 4 M15 8l4 4-4 4","var(--teal-ink,#0b6157)",[610,678,681],{"x":679,"y":646,"style":680},"380.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Column",[610,683,684],{"x":679,"y":613,"style":680},"formula",[610,686,687],{"x":679,"y":618,"style":619},"copied to new",[610,689,690],{"x":679,"y":656,"style":619},"rows",[42,692],{"x1":693,"y1":624,"x2":694,"y2":624,"stroke":626,"style":597},"442.6","464.6",[628,696],{"points":697,"fill":631},"468.6,80.0 459.6,85.0 459.6,75.0",[581,699],{"x":700,"y":591,"width":592,"height":593,"rx":594,"fill":701,"stroke":702,"style":597},"473.6","#fdefd8","var(--gold,#b4740a)",[599,704,706],{"transform":705},"translate(519.2,37.8) scale(1.000)",[603,707],{"d":708,"fill":606,"stroke":709,"style":608},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5","var(--gold-ink,#7a4e06)",[610,711,715],{"x":712,"y":713,"style":714},"531.2","84.2","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Widen ref +",[610,717,719],{"x":712,"y":718,"style":714},"101.8","autoFilter",[610,721,723],{"x":712,"y":722,"style":619},"121.2","A1:F6",[42,725],{"x1":726,"y1":624,"x2":727,"y2":624,"stroke":626,"style":597},"593.8","615.8",[628,729],{"points":730,"fill":631},"619.8,80.0 610.8,85.0 610.8,75.0",[581,732],{"x":733,"y":591,"width":592,"height":593,"rx":594,"fill":668,"stroke":669,"style":597},"624.8",[599,735,737],{"transform":736},"translate(670.4,29.0) scale(1.000)",[603,738],{"d":739,"fill":606,"stroke":676,"style":608},"M5 9h14 M4 15h14 M10 4l-2 16 M16 4l-2 16",[610,741,743],{"x":742,"y":646,"style":680},"682.4","Formula in",[610,745,746],{"x":742,"y":613,"style":680},"each row",[610,748,749],{"x":742,"y":618,"style":619},"same structured",[610,751,752],{"x":742,"y":656,"style":619},"reference",[25,754,756],{"id":755},"step-3-write-the-formula-into-every-row","Step 3 — Write the formula into every row",[10,758,759],{},"The column definition is a template for future rows; the existing rows still need the formula in their cells:",[30,761,763],{"className":79,"code":762,"language":81,"meta":35,"style":35},"for r in range(min_row + 1, max_row + 1):\n    cell = ws.cell(r, new_col, \"=\" + FORMULA)\n    cell.number_format = \"0.0%\"\nws.column_dimensions[letter].width = 11\nwb.save(\"products.xlsx\")\nprint(ws[f\"{letter}2\"].value[:60] + \"…\")\n",[37,764,765,792,813,823,833,842],{"__ignoreMap":35},[40,766,767,769,771,773,775,778,780,782,785,787,789],{"class":42,"line":43},[40,768,223],{"class":88},[40,770,226],{"class":92},[40,772,229],{"class":88},[40,774,232],{"class":197},[40,776,777],{"class":92},"(min_row ",[40,779,194],{"class":88},[40,781,240],{"class":197},[40,783,784],{"class":92},", max_row ",[40,786,194],{"class":88},[40,788,240],{"class":197},[40,790,791],{"class":92},"):\n",[40,793,794,797,799,802,805,808,811],{"class":42,"line":102},[40,795,796],{"class":92},"    cell ",[40,798,128],{"class":88},[40,800,801],{"class":92}," ws.cell(r, new_col, ",[40,803,804],{"class":50},"\"=\"",[40,806,807],{"class":88}," +",[40,809,810],{"class":197}," FORMULA",[40,812,137],{"class":92},[40,814,815,818,820],{"class":42,"line":115},[40,816,817],{"class":92},"    cell.number_format ",[40,819,128],{"class":88},[40,821,822],{"class":50}," \"0.0%\"\n",[40,824,825,828,830],{"class":42,"line":122},[40,826,827],{"class":92},"ws.column_dimensions[letter].width ",[40,829,128],{"class":88},[40,831,832],{"class":197}," 11\n",[40,834,835,838,840],{"class":42,"line":140},[40,836,837],{"class":92},"wb.save(",[40,839,134],{"class":50},[40,841,137],{"class":92},[40,843,844,846,849,852,854,856,858,860,863,866,869,872,874,877],{"class":42,"line":157},[40,845,266],{"class":197},[40,847,848],{"class":92},"(ws[",[40,850,851],{"class":88},"f",[40,853,475],{"class":50},[40,855,479],{"class":478},[40,857,499],{"class":92},[40,859,491],{"class":478},[40,861,862],{"class":50},"2\"",[40,864,865],{"class":92},"].value[:",[40,867,868],{"class":197},"60",[40,870,871],{"class":92},"] ",[40,873,194],{"class":88},[40,875,876],{"class":50}," \"…\"",[40,878,137],{"class":92},[10,880,881,882,885,886,889,890,894,895,898,899,902,903,429,906,429,909,912,913,916],{},"The formula uses fully qualified structured references — ",[37,883,884],{},"Products[[#This Row],[Price]]"," — rather than the ",[37,887,888],{},"[@Price]"," shorthand. Excel accepts both and may display the short form, but the full form works in every application that reads the file, as the recalculation below shows; ",[19,891,893],{"href":892},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-an-excel-table-with-formula-columns-using-xlsxwriter\u002F","Write an Excel Table with Formula Columns Using XlsxWriter"," compares the two in detail. ",[37,896,897],{},"IFERROR"," guards against a zero price, so a free sample does not fill the column with ",[37,900,901],{},"#DIV\u002F0!",". Column names that contain spaces or special characters are fine inside the brackets; names with ",[37,904,905],{},"[",[37,907,908],{},"]",[37,910,911],{},"#"," or ",[37,914,915],{},"'"," need escaping with an apostrophe, so it is simplest to avoid them in headers.",[562,918,571,924,571,927,571,930,571,935,571,941,571,945,571,951,571,955,571,959,571,963,571,967,571,971,571,975,571,979,571,983,571,987,571,991,571,995,571,999,571,1003,571,1008,571,1010,571,1014,571,1016,571,1019,571,1021,571,1024,571,1026,571,1029,571,1031,571,1034,571,1037,571,1041,571,1043,571,1046,571,1050,571,1052,571,1056,571,1058,571,1061,571,1063,571,1066,571,1068,571,1071,571,1073,571,1076,571,1078,571,1081,571,1083,571,1086,571,1089,571,1093,571,1095,571,1098,571,1100,571,1103,571,1105,571,1108,571,1110,571,1113,571,1115,571,1118,571,1120,571,1122,571,1124,571,1127,571,1130,571,1132,571,1135,571,1137,571,1140,571,1142,571,1144,571,1146,571,1149,571,1151,571,1154,571,1156,571,1158],{"viewBox":919,"role":565,"ariaLabelledBy":920,"xmlns":569,"style":923},"56 0 648 168",[921,922],"cc-table-t","cc-table-d","width:100%;max-width:648px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[573,925,926],{"id":921},"The Products table with its new column",[577,928,929],{"id":922},"The table now spans A1 to F6. Column F, Margin percent, holds the same structured-reference formula in every row and is registered as the table's calculated column, so a row added in Excel gets the formula automatically. The note in G2 is untouched.",[581,931],{"x":932,"y":584,"width":933,"height":934,"fill":587},"56","648","168",[581,936],{"x":937,"y":591,"width":938,"height":939,"rx":584,"fill":595,"stroke":596,"style":940},"72.4","32.0","24.0","stroke-width:1px",[581,942],{"x":943,"y":591,"width":944,"height":939,"rx":584,"fill":595,"stroke":596,"style":940},"104.4","68.1",[610,946,950],{"x":947,"y":948,"style":949},"138.5","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[581,952],{"x":953,"y":591,"width":954,"height":939,"rx":584,"fill":595,"stroke":596,"style":940},"172.5","79.0",[610,956,958],{"x":957,"y":948,"style":949},"212.0","B",[581,960],{"x":961,"y":591,"width":962,"height":939,"rx":584,"fill":595,"stroke":596,"style":940},"251.5","55.6",[610,964,966],{"x":965,"y":948,"style":949},"279.4","C",[581,968],{"x":969,"y":591,"width":970,"height":939,"rx":584,"fill":595,"stroke":596,"style":940},"307.2","78.9",[610,972,974],{"x":973,"y":948,"style":949},"346.6","D",[581,976],{"x":977,"y":591,"width":978,"height":939,"rx":584,"fill":595,"stroke":596,"style":940},"386.1","85.3",[610,980,982],{"x":981,"y":948,"style":949},"428.7","E",[581,984],{"x":985,"y":591,"width":986,"height":939,"rx":584,"fill":595,"stroke":596,"style":940},"471.3","107.8",[610,988,990],{"x":989,"y":948,"style":949},"525.2","F",[581,992],{"x":993,"y":591,"width":994,"height":939,"rx":584,"fill":595,"stroke":596,"style":940},"579.1","108.4",[610,996,998],{"x":997,"y":948,"style":949},"633.3","G",[581,1000],{"x":937,"y":1001,"width":938,"height":1002,"rx":584,"fill":595,"stroke":596,"style":940},"40.0","28.0",[610,1004,1007],{"x":1005,"y":1006,"style":949},"88.4","59.0","1",[581,1009],{"x":943,"y":1001,"width":944,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1011,1013],{"x":947,"y":1006,"style":1012},"font-size:12px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","SKU",[581,1015],{"x":953,"y":1001,"width":954,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1017,1018],{"x":957,"y":1006,"style":1012},"Product",[581,1020],{"x":961,"y":1001,"width":962,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1022,1023],{"x":965,"y":1006,"style":1012},"Price",[581,1025],{"x":969,"y":1001,"width":970,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1027,1028],{"x":973,"y":1006,"style":1012},"Unit cost",[581,1030],{"x":977,"y":1001,"width":978,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1032,1033],{"x":981,"y":1006,"style":1012},"Units sold",[581,1035],{"x":985,"y":1001,"width":986,"height":1002,"rx":584,"fill":668,"stroke":669,"style":1036},"stroke-width:1.5px",[610,1038,1040],{"x":989,"y":1006,"style":1039},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Margin %",[581,1042],{"x":993,"y":1001,"width":994,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[581,1044],{"x":937,"y":1045,"width":938,"height":1002,"rx":584,"fill":595,"stroke":596,"style":940},"68.0",[610,1047,1049],{"x":1005,"y":1048,"style":949},"87.0","2",[581,1051],{"x":943,"y":1045,"width":944,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1053,1055],{"x":947,"y":1048,"style":1054},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","B-100",[581,1057],{"x":953,"y":1045,"width":954,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1059,1060],{"x":957,"y":1048,"style":1054},"Bolts M8",[581,1062],{"x":961,"y":1045,"width":962,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1064,1065],{"x":965,"y":1048,"style":1054},"0.45",[581,1067],{"x":969,"y":1045,"width":970,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1069,1070],{"x":973,"y":1048,"style":1054},"0.21",[581,1072],{"x":977,"y":1045,"width":978,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1074,1075],{"x":981,"y":1048,"style":1054},"12000",[581,1077],{"x":985,"y":1045,"width":986,"height":1002,"rx":584,"fill":668,"stroke":669,"style":1036},[610,1079,1080],{"x":989,"y":1048,"style":1039},"=IFERROR(…)",[581,1082],{"x":993,"y":1045,"width":994,"height":1002,"rx":584,"fill":595,"stroke":596,"style":1036},[610,1084,1085],{"x":997,"y":1048,"style":1012},"notes go here",[581,1087],{"x":937,"y":1088,"width":938,"height":1002,"rx":584,"fill":595,"stroke":596,"style":940},"96.0",[610,1090,1092],{"x":1005,"y":1091,"style":949},"115.0","3",[581,1094],{"x":943,"y":1088,"width":944,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1096,1097],{"x":947,"y":1091,"style":1054},"N-200",[581,1099],{"x":953,"y":1088,"width":954,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1101,1102],{"x":957,"y":1091,"style":1054},"Nuts M8",[581,1104],{"x":961,"y":1088,"width":962,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1106,1107],{"x":965,"y":1091,"style":1054},"0.12",[581,1109],{"x":969,"y":1088,"width":970,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1111,1112],{"x":973,"y":1091,"style":1054},"0.05",[581,1114],{"x":977,"y":1088,"width":978,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1116,1117],{"x":981,"y":1091,"style":1054},"15500",[581,1119],{"x":985,"y":1088,"width":986,"height":1002,"rx":584,"fill":668,"stroke":669,"style":1036},[610,1121,1080],{"x":989,"y":1091,"style":1039},[581,1123],{"x":993,"y":1088,"width":994,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[581,1125],{"x":937,"y":1126,"width":938,"height":1002,"rx":584,"fill":595,"stroke":596,"style":940},"124.0",[610,1128,583],{"x":1005,"y":1129,"style":949},"143.0",[581,1131],{"x":943,"y":1126,"width":944,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1133,1134],{"x":947,"y":1129,"style":1054},"W-300",[581,1136],{"x":953,"y":1126,"width":954,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1138,1139],{"x":957,"y":1129,"style":1054},"Washers",[581,1141],{"x":961,"y":1126,"width":962,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1143,1112],{"x":965,"y":1129,"style":1054},[581,1145],{"x":969,"y":1126,"width":970,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1147,1148],{"x":973,"y":1129,"style":1054},"0.02",[581,1150],{"x":977,"y":1126,"width":978,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[610,1152,1153],{"x":981,"y":1129,"style":1054},"30000",[581,1155],{"x":985,"y":1126,"width":986,"height":1002,"rx":584,"fill":668,"stroke":669,"style":1036},[610,1157,1080],{"x":989,"y":1129,"style":1039},[581,1159],{"x":993,"y":1126,"width":994,"height":1002,"rx":584,"fill":587,"stroke":596,"style":940},[25,1161,1163],{"id":1162},"step-4-a-second-column-a-total-and-a-recalculation-check","Step 4 — A second column, a total, and a recalculation check",[10,1165,1166],{},"Wrap the steps in a function so further columns are one call each, add a revenue-weighted margin and check everything by recalculating the workbook:",[30,1168,1170],{"className":79,"code":1169,"language":81,"meta":35,"style":35},"def add_calculated_column(ws, table_name, name, formula, number_format):\n    table = ws.tables[table_name]\n    c1, r1, c2, r2 = range_boundaries(table.ref)\n    col = c2 + 1\n    if any(ws.cell(r, col).value is not None for r in range(r1, r2 + 1)):\n        raise ValueError(f\"column {get_column_letter(col)} is not empty\")\n    ws.cell(r1, col, name)\n    tc = TableColumn(id=max(c.id for c in table.tableColumns) + 1, name=name)\n    tc.calculatedColumnFormula = TableFormula(attr_text=formula)\n    table.tableColumns.append(tc)\n    table.ref = table.autoFilter.ref = f\"{get_column_letter(c1)}{r1}:{get_column_letter(col)}{r2}\"\n    for r in range(r1 + 1, r2 + 1):\n        ws.cell(r, col, \"=\" + formula).number_format = number_format\n\nwb = load_workbook(\"products.xlsx\")\nws = wb[\"Products\"]\ntry:\n    add_calculated_column(ws, \"Products\", \"Gross profit\",\n                          \"(Products[[#This Row],[Price]]-Products[[#This Row],[Unit cost]])*Products[[#This Row],[Units sold]]\", \"#,##0.00\")\nexcept ValueError as e:\n    print(\"refused:\", e)\nws[\"A9\"], ws[\"B9\"] = \"Total gross profit\", \"=SUM(Products[Gross profit])\"\nwb.save(\"products.xlsx\")\n",[37,1171,1172,1184,1194,1203,1217,1253,1280,1285,1323,1339,1344,1387,1414,1432,1437,1450,1463,1472,1488,1501,1515,1529,1556],{"__ignoreMap":35},[40,1173,1174,1177,1181],{"class":42,"line":43},[40,1175,1176],{"class":88},"def",[40,1178,1180],{"class":1179},"s_Opv"," add_calculated_column",[40,1182,1183],{"class":92},"(ws, table_name, name, formula, number_format):\n",[40,1185,1186,1189,1191],{"class":42,"line":102},[40,1187,1188],{"class":92},"    table ",[40,1190,128],{"class":88},[40,1192,1193],{"class":92}," ws.tables[table_name]\n",[40,1195,1196,1199,1201],{"class":42,"line":115},[40,1197,1198],{"class":92},"    c1, r1, c2, r2 ",[40,1200,128],{"class":88},[40,1202,180],{"class":92},[40,1204,1205,1208,1210,1213,1215],{"class":42,"line":122},[40,1206,1207],{"class":92},"    col ",[40,1209,128],{"class":88},[40,1211,1212],{"class":92}," c2 ",[40,1214,194],{"class":88},[40,1216,198],{"class":197},[40,1218,1219,1222,1225,1228,1230,1232,1234,1237,1239,1241,1243,1246,1248,1250],{"class":42,"line":140},[40,1220,1221],{"class":88},"    if",[40,1223,1224],{"class":197}," any",[40,1226,1227],{"class":92},"(ws.cell(r, col).value ",[40,1229,252],{"class":88},[40,1231,255],{"class":88},[40,1233,258],{"class":197},[40,1235,1236],{"class":88}," for",[40,1238,226],{"class":92},[40,1240,229],{"class":88},[40,1242,232],{"class":197},[40,1244,1245],{"class":92},"(r1, r2 ",[40,1247,194],{"class":88},[40,1249,240],{"class":197},[40,1251,1252],{"class":92},")):\n",[40,1254,1255,1258,1261,1263,1265,1268,1270,1273,1275,1278],{"class":42,"line":157},[40,1256,1257],{"class":88},"        raise",[40,1259,1260],{"class":197}," ValueError",[40,1262,269],{"class":92},[40,1264,851],{"class":88},[40,1266,1267],{"class":50},"\"column ",[40,1269,479],{"class":478},[40,1271,1272],{"class":92},"get_column_letter(col)",[40,1274,491],{"class":478},[40,1276,1277],{"class":50}," is not empty\"",[40,1279,137],{"class":92},[40,1281,1282],{"class":42,"line":172},[40,1283,1284],{"class":92},"    ws.cell(r1, col, name)\n",[40,1286,1287,1290,1292,1294,1296,1298,1300,1302,1304,1306,1308,1310,1312,1314,1316,1318,1320],{"class":42,"line":183},[40,1288,1289],{"class":92},"    tc ",[40,1291,128],{"class":88},[40,1293,401],{"class":92},[40,1295,405],{"class":404},[40,1297,128],{"class":88},[40,1299,410],{"class":197},[40,1301,413],{"class":92},[40,1303,223],{"class":88},[40,1305,313],{"class":92},[40,1307,229],{"class":88},[40,1309,422],{"class":92},[40,1311,194],{"class":88},[40,1313,240],{"class":197},[40,1315,429],{"class":92},[40,1317,432],{"class":404},[40,1319,128],{"class":88},[40,1321,1322],{"class":92},"name)\n",[40,1324,1325,1328,1330,1332,1334,1336],{"class":42,"line":201},[40,1326,1327],{"class":92},"    tc.calculatedColumnFormula ",[40,1329,128],{"class":88},[40,1331,448],{"class":92},[40,1333,451],{"class":404},[40,1335,128],{"class":88},[40,1337,1338],{"class":92},"formula)\n",[40,1340,1341],{"class":42,"line":212},[40,1342,1343],{"class":92},"    table.tableColumns.append(tc)\n",[40,1345,1346,1349,1351,1354,1356,1358,1360,1362,1365,1367,1370,1372,1374,1376,1378,1380,1383,1385],{"class":42,"line":263},[40,1347,1348],{"class":92},"    table.ref ",[40,1350,128],{"class":88},[40,1352,1353],{"class":92}," table.autoFilter.ref ",[40,1355,128],{"class":88},[40,1357,472],{"class":88},[40,1359,475],{"class":50},[40,1361,479],{"class":478},[40,1363,1364],{"class":92},"get_column_letter(c1)",[40,1366,485],{"class":478},[40,1368,1369],{"class":92},"r1",[40,1371,491],{"class":478},[40,1373,494],{"class":50},[40,1375,479],{"class":478},[40,1377,1272],{"class":92},[40,1379,485],{"class":478},[40,1381,1382],{"class":92},"r2",[40,1384,491],{"class":478},[40,1386,509],{"class":50},[40,1388,1389,1392,1394,1396,1398,1401,1403,1405,1408,1410,1412],{"class":42,"line":298},[40,1390,1391],{"class":88},"    for",[40,1393,226],{"class":92},[40,1395,229],{"class":88},[40,1397,232],{"class":197},[40,1399,1400],{"class":92},"(r1 ",[40,1402,194],{"class":88},[40,1404,240],{"class":197},[40,1406,1407],{"class":92},", r2 ",[40,1409,194],{"class":88},[40,1411,240],{"class":197},[40,1413,791],{"class":92},[40,1415,1417,1420,1422,1424,1427,1429],{"class":42,"line":1416},13,[40,1418,1419],{"class":92},"        ws.cell(r, col, ",[40,1421,804],{"class":50},[40,1423,807],{"class":88},[40,1425,1426],{"class":92}," formula).number_format ",[40,1428,128],{"class":88},[40,1430,1431],{"class":92}," number_format\n",[40,1433,1435],{"class":42,"line":1434},14,[40,1436,119],{"emptyLinePlaceholder":118},[40,1438,1440,1442,1444,1446,1448],{"class":42,"line":1439},15,[40,1441,125],{"class":92},[40,1443,128],{"class":88},[40,1445,131],{"class":92},[40,1447,134],{"class":50},[40,1449,137],{"class":92},[40,1451,1453,1455,1457,1459,1461],{"class":42,"line":1452},16,[40,1454,143],{"class":92},[40,1456,128],{"class":88},[40,1458,148],{"class":92},[40,1460,151],{"class":50},[40,1462,154],{"class":92},[40,1464,1466,1469],{"class":42,"line":1465},17,[40,1467,1468],{"class":88},"try",[40,1470,1471],{"class":92},":\n",[40,1473,1475,1478,1480,1482,1485],{"class":42,"line":1474},18,[40,1476,1477],{"class":92},"    add_calculated_column(ws, ",[40,1479,151],{"class":50},[40,1481,429],{"class":92},[40,1483,1484],{"class":50},"\"Gross profit\"",[40,1486,1487],{"class":92},",\n",[40,1489,1491,1494,1496,1499],{"class":42,"line":1490},19,[40,1492,1493],{"class":50},"                          \"(Products[[#This Row],[Price]]-Products[[#This Row],[Unit cost]])*Products[[#This Row],[Units sold]]\"",[40,1495,429],{"class":92},[40,1497,1498],{"class":50},"\"#,##0.00\"",[40,1500,137],{"class":92},[40,1502,1504,1507,1509,1512],{"class":42,"line":1503},20,[40,1505,1506],{"class":88},"except",[40,1508,1260],{"class":197},[40,1510,1511],{"class":88}," as",[40,1513,1514],{"class":92}," e:\n",[40,1516,1518,1521,1523,1526],{"class":42,"line":1517},21,[40,1519,1520],{"class":197},"    print",[40,1522,269],{"class":92},[40,1524,1525],{"class":50},"\"refused:\"",[40,1527,1528],{"class":92},", e)\n",[40,1530,1532,1535,1538,1541,1544,1546,1548,1551,1553],{"class":42,"line":1531},22,[40,1533,1534],{"class":92},"ws[",[40,1536,1537],{"class":50},"\"A9\"",[40,1539,1540],{"class":92},"], ws[",[40,1542,1543],{"class":50},"\"B9\"",[40,1545,871],{"class":92},[40,1547,128],{"class":88},[40,1549,1550],{"class":50}," \"Total gross profit\"",[40,1552,429],{"class":92},[40,1554,1555],{"class":50},"\"=SUM(Products[Gross profit])\"\n",[40,1557,1559,1561,1563],{"class":42,"line":1558},23,[40,1560,837],{"class":92},[40,1562,134],{"class":50},[40,1564,137],{"class":92},[10,1566,1567],{},"The function refuses to add the second column: column G is not empty, because of the note in G2. That is the safety check doing its job — the note has to be moved before the table can grow further. Move it and try again:",[30,1569,1571],{"className":79,"code":1570,"language":81,"meta":35,"style":35},"import subprocess\nimport pandas as pd\n\nws[\"I2\"], ws[\"G2\"] = ws[\"G2\"].value, None\nadd_calculated_column(ws, \"Products\", \"Gross profit\",\n                      \"(Products[[#This Row],[Price]]-Products[[#This Row],[Unit cost]])*Products[[#This Row],[Units sold]]\", \"#,##0.00\")\nwb.save(\"products.xlsx\")\n\nload_workbook(\"products.xlsx\").save(\"roundtrip.xlsx\")\nsubprocess.run([\"soffice\", \"--headless\", \"--convert-to\", \"xlsx\", \"--outdir\", \"calc\", \"roundtrip.xlsx\"], check=True, capture_output=True)\ncalc = load_workbook(\"calc\u002Froundtrip.xlsx\", data_only=True)[\"Products\"]\nrows = [[c.value for c in r[:7]] for r in calc.iter_rows(min_row=1, max_row=6)]\ndf = pd.DataFrame(rows[1:], columns=rows[0])\nexpected = ((df[\"Price\"] - df[\"Unit cost\"]) * df[\"Units sold\"]).round(2)\nprint(df[[\"SKU\", \"Margin %\", \"Gross profit\"]].round(3).to_string(index=False))\nprint(\"gross profit matches:\", (df[\"Gross profit\"].round(2) == expected).all(), \"| total:\", calc[\"B9\"].value, \"| table now\", ws.tables[\"Products\"].ref)\n",[37,1572,1573,1580,1593,1597,1624,1637,1648,1656,1660,1675,1734,1762,1814,1842,1884,1922],{"__ignoreMap":35},[40,1574,1575,1577],{"class":42,"line":43},[40,1576,96],{"class":88},[40,1578,1579],{"class":92}," subprocess\n",[40,1581,1582,1584,1587,1590],{"class":42,"line":102},[40,1583,96],{"class":88},[40,1585,1586],{"class":92}," pandas ",[40,1588,1589],{"class":88},"as",[40,1591,1592],{"class":92}," pd\n",[40,1594,1595],{"class":42,"line":115},[40,1596,119],{"emptyLinePlaceholder":118},[40,1598,1599,1601,1604,1606,1609,1611,1613,1616,1618,1621],{"class":42,"line":122},[40,1600,1534],{"class":92},[40,1602,1603],{"class":50},"\"I2\"",[40,1605,1540],{"class":92},[40,1607,1608],{"class":50},"\"G2\"",[40,1610,871],{"class":92},[40,1612,128],{"class":88},[40,1614,1615],{"class":92}," ws[",[40,1617,1608],{"class":50},[40,1619,1620],{"class":92},"].value, ",[40,1622,1623],{"class":197},"None\n",[40,1625,1626,1629,1631,1633,1635],{"class":42,"line":140},[40,1627,1628],{"class":92},"add_calculated_column(ws, ",[40,1630,151],{"class":50},[40,1632,429],{"class":92},[40,1634,1484],{"class":50},[40,1636,1487],{"class":92},[40,1638,1639,1642,1644,1646],{"class":42,"line":157},[40,1640,1641],{"class":50},"                      \"(Products[[#This Row],[Price]]-Products[[#This Row],[Unit cost]])*Products[[#This Row],[Units sold]]\"",[40,1643,429],{"class":92},[40,1645,1498],{"class":50},[40,1647,137],{"class":92},[40,1649,1650,1652,1654],{"class":42,"line":172},[40,1651,837],{"class":92},[40,1653,134],{"class":50},[40,1655,137],{"class":92},[40,1657,1658],{"class":42,"line":183},[40,1659,119],{"emptyLinePlaceholder":118},[40,1661,1662,1665,1667,1670,1673],{"class":42,"line":201},[40,1663,1664],{"class":92},"load_workbook(",[40,1666,134],{"class":50},[40,1668,1669],{"class":92},").save(",[40,1671,1672],{"class":50},"\"roundtrip.xlsx\"",[40,1674,137],{"class":92},[40,1676,1677,1680,1683,1685,1688,1690,1693,1695,1698,1700,1703,1705,1708,1710,1712,1715,1718,1720,1723,1725,1728,1730,1732],{"class":42,"line":212},[40,1678,1679],{"class":92},"subprocess.run([",[40,1681,1682],{"class":50},"\"soffice\"",[40,1684,429],{"class":92},[40,1686,1687],{"class":50},"\"--headless\"",[40,1689,429],{"class":92},[40,1691,1692],{"class":50},"\"--convert-to\"",[40,1694,429],{"class":92},[40,1696,1697],{"class":50},"\"xlsx\"",[40,1699,429],{"class":92},[40,1701,1702],{"class":50},"\"--outdir\"",[40,1704,429],{"class":92},[40,1706,1707],{"class":50},"\"calc\"",[40,1709,429],{"class":92},[40,1711,1672],{"class":50},[40,1713,1714],{"class":92},"], ",[40,1716,1717],{"class":404},"check",[40,1719,128],{"class":88},[40,1721,1722],{"class":197},"True",[40,1724,429],{"class":92},[40,1726,1727],{"class":404},"capture_output",[40,1729,128],{"class":88},[40,1731,1722],{"class":197},[40,1733,137],{"class":92},[40,1735,1736,1739,1741,1743,1746,1748,1751,1753,1755,1758,1760],{"class":42,"line":263},[40,1737,1738],{"class":92},"calc ",[40,1740,128],{"class":88},[40,1742,131],{"class":92},[40,1744,1745],{"class":50},"\"calc\u002Froundtrip.xlsx\"",[40,1747,429],{"class":92},[40,1749,1750],{"class":404},"data_only",[40,1752,128],{"class":88},[40,1754,1722],{"class":197},[40,1756,1757],{"class":92},")[",[40,1759,151],{"class":50},[40,1761,154],{"class":92},[40,1763,1764,1767,1769,1772,1774,1776,1778,1781,1784,1787,1789,1791,1793,1796,1798,1800,1802,1804,1806,1808,1811],{"class":42,"line":298},[40,1765,1766],{"class":92},"rows ",[40,1768,128],{"class":88},[40,1770,1771],{"class":92}," [[c.value ",[40,1773,223],{"class":88},[40,1775,313],{"class":92},[40,1777,229],{"class":88},[40,1779,1780],{"class":92}," r[:",[40,1782,1783],{"class":197},"7",[40,1785,1786],{"class":92},"]] ",[40,1788,223],{"class":88},[40,1790,226],{"class":92},[40,1792,229],{"class":88},[40,1794,1795],{"class":92}," calc.iter_rows(",[40,1797,488],{"class":404},[40,1799,128],{"class":88},[40,1801,1007],{"class":197},[40,1803,429],{"class":92},[40,1805,504],{"class":404},[40,1807,128],{"class":88},[40,1809,1810],{"class":197},"6",[40,1812,1813],{"class":92},")]\n",[40,1815,1816,1819,1821,1824,1826,1829,1832,1834,1837,1839],{"class":42,"line":1416},[40,1817,1818],{"class":92},"df ",[40,1820,128],{"class":88},[40,1822,1823],{"class":92}," pd.DataFrame(rows[",[40,1825,1007],{"class":197},[40,1827,1828],{"class":92},":], ",[40,1830,1831],{"class":404},"columns",[40,1833,128],{"class":88},[40,1835,1836],{"class":92},"rows[",[40,1838,584],{"class":197},[40,1840,1841],{"class":92},"])\n",[40,1843,1844,1847,1849,1852,1855,1857,1860,1863,1866,1869,1872,1874,1877,1880,1882],{"class":42,"line":1434},[40,1845,1846],{"class":92},"expected ",[40,1848,128],{"class":88},[40,1850,1851],{"class":92}," ((df[",[40,1853,1854],{"class":50},"\"Price\"",[40,1856,871],{"class":92},[40,1858,1859],{"class":88},"-",[40,1861,1862],{"class":92}," df[",[40,1864,1865],{"class":50},"\"Unit cost\"",[40,1867,1868],{"class":92},"]) ",[40,1870,1871],{"class":88},"*",[40,1873,1862],{"class":92},[40,1875,1876],{"class":50},"\"Units sold\"",[40,1878,1879],{"class":92},"]).round(",[40,1881,1049],{"class":197},[40,1883,137],{"class":92},[40,1885,1886,1888,1891,1894,1896,1899,1901,1903,1906,1908,1911,1914,1916,1919],{"class":42,"line":1439},[40,1887,266],{"class":197},[40,1889,1890],{"class":92},"(df[[",[40,1892,1893],{"class":50},"\"SKU\"",[40,1895,429],{"class":92},[40,1897,1898],{"class":50},"\"Margin %\"",[40,1900,429],{"class":92},[40,1902,1484],{"class":50},[40,1904,1905],{"class":92},"]].round(",[40,1907,1092],{"class":197},[40,1909,1910],{"class":92},").to_string(",[40,1912,1913],{"class":404},"index",[40,1915,128],{"class":88},[40,1917,1918],{"class":197},"False",[40,1920,1921],{"class":92},"))\n",[40,1923,1924,1926,1928,1931,1934,1936,1939,1941,1943,1946,1949,1952,1955,1957,1959,1962,1965,1967],{"class":42,"line":1452},[40,1925,266],{"class":197},[40,1927,269],{"class":92},[40,1929,1930],{"class":50},"\"gross profit matches:\"",[40,1932,1933],{"class":92},", (df[",[40,1935,1484],{"class":50},[40,1937,1938],{"class":92},"].round(",[40,1940,1049],{"class":197},[40,1942,243],{"class":92},[40,1944,1945],{"class":88},"==",[40,1947,1948],{"class":92}," expected).all(), ",[40,1950,1951],{"class":50},"\"| total:\"",[40,1953,1954],{"class":92},", calc[",[40,1956,1543],{"class":50},[40,1958,1620],{"class":92},[40,1960,1961],{"class":50},"\"| table now\"",[40,1963,1964],{"class":92},", ws.tables[",[40,1966,151],{"class":50},[40,1968,1969],{"class":92},"].ref)\n",[10,1971,1972],{},"After the note moves to I2, the table grows to A1:G6. The margins range from 34% on brackets to 60% on washers, and the gross profit column — price less cost, times units — totals 7,415, with bolts contributing the most at 2,880 despite their middling margin, because they sell in volume. Moving content out of the way is a decision for the code's author, not something to automate silently. Recalculating with LibreOffice gives margins and gross profits that match the same calculation in pandas, with the total below the table summing the new column by name. Rows added to the table later in Excel will get both formulas automatically, because each column carries its calculated-column formula.",[10,1974,1975,1976,1979,1980,1984],{},"The results quoted here come from the recalculated copy. The workbook openpyxl saved holds formulas without results, so reading it straight back with pandas, or with openpyxl's ",[37,1977,1978],{},"data_only=True",", gives empty cells in the new columns until the file has been opened and saved in Excel or recalculated as above. If a later step of the same job needs the margins as numbers, compute them in pandas from the same columns rather than reading the formulas back; ",[19,1981,1983],{"href":1982},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fread-an-excel-table-by-name-into-pandas\u002F","Read an Excel Table by Name into pandas"," shows how to load the table itself.",[562,1986,571,1991,571,1994,571,1997,571,2000,571,2004,571,2008,571,2011,571,2015,571,2019,571,2022,571,2025,571,2033,571,2038,571,2042,571,2048,571,2051,571,2055,571,2057,571,2060,571,2064,571,2067,571,2070,571,2075,571,2078],{"viewBox":1987,"role":565,"ariaLabelledBy":1988,"xmlns":569,"style":570},"4 0 752 272",[1989,1990],"cc-choose-t","cc-choose-d",[573,1992,1993],{"id":1989},"Values or a calculated column?",[577,1995,1996],{"id":1990},"If the derived figure should update when people edit the table or add rows in Excel, add a calculated column with a formula. If it is a snapshot computed once in Python, such as a score from an external model, write values in a normal column of the table.",[581,1998],{"x":583,"y":584,"width":585,"height":1999,"fill":587},"272",[581,2001],{"x":2002,"y":591,"width":2003,"height":646,"rx":594,"fill":635,"stroke":626,"style":597},"216.2","327.7",[599,2005,2006],{"transform":672},[603,2007],{"d":641,"fill":606,"stroke":642,"style":608},[610,2009,2010],{"x":679,"y":646,"style":647},"Should the figure follow edits in Excel?",[42,2012],{"x1":679,"y1":2013,"x2":679,"y2":2014,"stroke":626,"style":597},"91.5","111.5",[42,2016],{"x1":2017,"y1":2014,"x2":2018,"y2":2014,"stroke":626,"style":597},"196.0","564.0",[42,2020],{"x1":2017,"y1":2014,"x2":2017,"y2":2021,"stroke":626,"style":597},"157.5",[628,2023],{"points":2024,"fill":631},"196.0,161.5 191.0,152.5 201.0,152.5",[581,2026],{"x":2027,"y":2028,"width":2029,"height":2030,"rx":2031,"fill":587,"stroke":596,"style":2032},"176.4","121.5","39.2","21.0","10","stroke-width:1.2px",[610,2034,2037],{"x":2017,"y":2035,"style":2036},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","yes",[581,2039],{"x":590,"y":2040,"width":2041,"height":613,"rx":594,"fill":668,"stroke":669,"style":597},"163.5","352.0",[599,2043,2045],{"transform":2044},"translate(184.0,176.5) scale(1.000)",[603,2046],{"d":2047,"fill":606,"stroke":676,"style":608},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7",[610,2049,16],{"x":2017,"y":2050,"style":680},"223.0",[610,2052,2054],{"x":2017,"y":2053,"style":619},"242.5","formula + calculatedColumnFormula",[42,2056],{"x1":2018,"y1":2014,"x2":2018,"y2":2021,"stroke":626,"style":597},[628,2058],{"points":2059,"fill":631},"564.0,161.5 559.0,152.5 569.0,152.5",[581,2061],{"x":2062,"y":2028,"width":2063,"height":2030,"rx":2031,"fill":587,"stroke":596,"style":2032},"511.6","104.7",[610,2065,2066],{"x":2018,"y":2035,"style":2036},"no, a snapshot",[581,2068],{"x":2069,"y":2040,"width":2041,"height":613,"rx":594,"fill":635,"stroke":626,"style":597},"388.0",[599,2071,2073],{"transform":2072},"translate(552.0,176.5) scale(1.000)",[603,2074],{"d":739,"fill":606,"stroke":642,"style":608},[610,2076,2077],{"x":2018,"y":2050,"style":647},"values in a table column",[610,2079,2080],{"x":2018,"y":2053,"style":619},"TableColumn without formula",[25,2082,2084],{"id":2083},"common-pitfalls","Common pitfalls",[2086,2087,2088,2096,2107,2116,2122],"ul",{},[2089,2090,2091,2095],"li",{},[2092,2093,2094],"strong",{},"Writing values beside the table."," They are not part of it; new rows get nothing.",[2089,2097,2098,2103,2104,2106],{},[2092,2099,2100,2101,23],{},"Forgetting ",[37,2102,552],{}," Widen it with ",[37,2105,548],{},", or Excel repairs the file.",[2089,2108,2109,2112,2113,2115],{},[2092,2110,2111],{},"Mismatched names."," The ",[37,2114,540],{}," name must equal the header text.",[2089,2117,2118,2121],{},[2092,2119,2120],{},"Overwriting neighbouring cells."," Check the new column is empty first.",[2089,2123,2124,2127,2128,2131],{},[2092,2125,2126],{},"Shorthand references."," Use ",[37,2129,2130],{},"Table[[#This Row],[Column]]"," for files read outside Excel.",[25,2133,2135],{"id":2134},"conclusion","Conclusion",[10,2137,2138,2139,2141,2142,2144,2145,549,2148,2151],{},"To add a calculated column to an existing table with openpyxl, check the column to its right is empty, write the header, append a ",[37,2140,540],{}," with the same name, the next id and a ",[37,2143,544],{},", widen both ",[37,2146,2147],{},"table.ref",[37,2149,2150],{},"table.autoFilter.ref",", and write the formula — with fully qualified structured references — into every data row. Wrap it in a function, refuse to overwrite other content, and recalculate to check the results.",[25,2153,2155],{"id":2154},"frequently-asked-questions","Frequently asked questions",[10,2157,2158,2161],{},[2092,2159,2160],{},"How do I add a column to an existing Excel table with openpyxl?","\nWrite the header next to the table, append a TableColumn with that name to table.tableColumns, and extend table.ref and table.autoFilter.ref to include the new column.",[10,2163,2164,2167],{},[2092,2165,2166],{},"How do I make Excel fill a formula into new table rows?","\nSet the column's calculatedColumnFormula to the formula without the leading equals sign; Excel copies it into rows added to the table.",[10,2169,2170,2173],{},[2092,2171,2172],{},"Why does Excel repair my workbook after I changed a table with openpyxl?","\nThe table's ref, autoFilter ref, column list and header cells must agree; a column in the header but missing from tableColumns, or a ref that was not widened, triggers a repair.",[10,2175,2176,2186],{},[2092,2177,2178,2179,2182,2183,2185],{},"Should I use ",[40,2180,2181],{},"@Column"," or Table[[#This Row],",[40,2184,681],{},"] in openpyxl formulas?","\nThe fully qualified form works in Excel and in other applications such as LibreOffice, so it is the safer choice for generated files.",[25,2188,2190],{"id":2189},"related","Related",[2086,2192,2193,2199,2206,2213],{},[2089,2194,2195,2196,2198],{},"Up one level: ",[19,2197,22],{"href":21}," — tables from Python.",[2089,2200,2201,2205],{},[19,2202,2204],{"href":2203},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fcreate-an-excel-table-with-python\u002F","Create an Excel Table with Python"," — creating the table.",[2089,2207,2208,2212],{},[19,2209,2211],{"href":2210},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fappend-rows-to-an-excel-table-and-resize-it\u002F","Append Rows to an Excel Table and Resize It"," — growing it downwards.",[2089,2214,2215,2219],{},[19,2216,2218],{"href":2217},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-a-total-row-to-an-excel-table\u002F","Add a Total Row to an Excel Table"," — totals inside the table.",[2221,2222,2223],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .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 .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":35,"searchDepth":102,"depth":102,"links":2225},[2226,2227,2228,2229,2230,2231,2232,2233,2234],{"id":27,"depth":102,"text":28},{"id":75,"depth":102,"text":76},{"id":335,"depth":102,"text":336},{"id":755,"depth":102,"text":756},{"id":1162,"depth":102,"text":1163},{"id":2083,"depth":102,"text":2084},{"id":2134,"depth":102,"text":2135},{"id":2154,"depth":102,"text":2155},{"id":2189,"depth":102,"text":2190},"2026-10-02","Add a calculated column to an existing Excel table with openpyxl: find the table, widen its range and autofilter, register the new column with a calculated-column formula so Excel fills it into rows added later, write the formula into every row with portable structured references, add a total, and verify the results by recalculating.","md",[2239,2241,2243,2245],{"q":2160,"a":2240},"Write the header next to the table, append a TableColumn with that name to table.tableColumns, and extend table.ref and table.autoFilter.ref to include the new column.",{"q":2166,"a":2242},"Set the column's calculatedColumnFormula to the formula without the leading equals sign; Excel copies it into rows added to the table.",{"q":2172,"a":2244},"The table's ref, autoFilter ref, column list and header cells must agree; a column in the header but missing from tableColumns, or a ref that was not widened, triggers a repair.",{"q":2246,"a":2247},"Should I use [@Column] or Table[[#This Row],[Column]] in openpyxl formulas?","The fully qualified form works in Excel and in other applications such as LibreOffice, so it is the safer choice for generated files.","add calculated columns to an excel table with python",null,{"breadcrumb":2251},[2252,2255,2258],{"name":2253,"item":2254},"Home","\u002F",{"name":2256,"item":2257},"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\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-calculated-columns-to-an-excel-table-with-python",{"title":2261,"description":2262},"Add Calculated Columns to an Excel Table with openpyxl","Extend an existing Excel table with openpyxl: ws.tables, resizing ref and autoFilter, TableColumn with calculatedColumnFormula, structured references per row, totals row functions, and a recalculation check.","add-calculated-columns-to-an-excel-table-with-python","formatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-calculated-columns-to-an-excel-table-with-python\u002Findex",[2266,2267],"tables-and-filters","excel-formulas","how-to","M5a3i_orWOeMusKfyBjQSZXbPqCzsbt2jeO8qO2QY8E",[2271,2275],{"title":2272,"path":2273,"stem":2274,"children":-1},"Add an Autofilter to an Excel Sheet with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-an-autofilter-to-an-excel-sheet-with-openpyxl","formatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-an-autofilter-to-an-excel-sheet-with-openpyxl\u002Findex",{"title":2276,"path":2277,"stem":2278,"children":-1},"Append Rows to an Excel Table and Resize It with openpyxl","\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fappend-rows-to-an-excel-table-and-resize-it","formatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fappend-rows-to-an-excel-table-and-resize-it\u002Findex",[2280,2282],{"path":2281,"title":22},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python",{"path":2283,"title":2256},"\u002Fformatting-and-charting-excel-reports-with-python",[],[2286,2289,2292,2294,2297,2300],{"path":2287,"title":2288,"kind":2268},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fapply-conditional-formatting-to-an-excel-table-that-grows","Apply Conditional Formatting to an Excel Table That Grows",{"path":2290,"title":2291,"kind":2268},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fsort-and-filter-an-excel-online-table-with-microsoft-graph","Sort and Filter an Excel Online Table with Microsoft Graph",{"path":2293,"title":893,"kind":2268},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fwrite-an-excel-table-with-formula-columns-using-xlsxwriter",{"path":2295,"title":2296,"kind":2268},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-charts-in-excel-with-openpyxl\u002Fmake-an-excel-chart-grow-with-its-data","Make an Excel Chart Grow with Its Data",{"path":2298,"title":2299,"kind":2268},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fcreating-excel-tables-and-autofilters-with-python\u002Fadd-a-total-row-to-an-excel-table","Add a Total Row to an Excel Table with Python",{"path":2273,"title":2272,"kind":2268},1790972116253]