[{"data":1,"prerenderedAt":1781},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fturn-an-excel-model-into-a-python-function":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fturn-an-excel-model-into-a-python-function":1747,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fturn-an-excel-model-into-a-python-function":1755,"children:\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fturn-an-excel-model-into-a-python-function":1760,"related:\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fturn-an-excel-model-into-a-python-function":1761},{"id":4,"title":5,"body":6,"dateModified":1713,"datePublished":1713,"description":1714,"extension":1715,"faq":1716,"keyword":1725,"kind":1726,"meta":1727,"navigation":94,"path":1736,"seo":1737,"slug":1740,"stem":1741,"topics":1742,"type":1745,"__hash__":1746},"docs\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fturn-an-excel-model-into-a-python-function\u002Findex.md","Turn an Excel Model into a Python Function",{"type":7,"value":8,"toc":1700},"minimark",[9,29,34,65,68,72,284,302,306,368,379,533,537,693,702,874,878,1187,1190,1194,1437,1469,1473,1490,1563,1567,1615,1619,1629,1633,1642,1652,1661,1667,1671,1696],[10,11,12,13,17,18,22,23,28],"p",{},"Many decisions run on an Excel model: a pricing sheet, a business case, a capacity calculation. The model belongs in Excel because the people who own the assumptions can read and change it there. But some questions need it run many times — what happens across two thousand combinations of volume and price, which inputs move profit the most, how likely a loss is — and recalculating a workbook two thousand times through Excel or LibreOffice is slow and fragile. The ",[14,15,16],"code",{},"formulas"," library offers another route. It reads a workbook's formulas, builds the dependency graph between cells, and can ",[19,20,21],"em",{},"compile"," the part of it between chosen input cells and output cells into an ordinary Python function. The workbook stays the single source of the logic; Python just calls it. This page compiles a small profit model, checks it against LibreOffice, runs a Monte Carlo simulation with it, and shows how to detect formulas the library cannot evaluate. It belongs to ",[24,25,27],"a",{"href":26},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002F","Working with Excel Formulas in Python",".",[30,31,33],"h2",{"id":32},"prerequisites","Prerequisites",[35,36,41],"pre",{"className":37,"code":38,"language":39,"meta":40,"style":40},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install formulas openpyxl numpy\n","bash","",[14,42,43],{"__ignoreMap":40},[44,45,48,52,56,59,62],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," formulas",[44,60,61],{"class":54}," openpyxl",[44,63,64],{"class":54}," numpy\n",[10,66,67],{},"The model is a single sheet: five inputs in B1:B5 — units, price, unit cost, fixed costs and tax rate — and formulas below them for revenue, costs, profit, tax and net margin. LibreOffice is used once to confirm the library's answers.",[30,69,71],{"id":70},"step-1-load-the-model-and-calculate-it-once","Step 1 — Load the model and calculate it once",[35,73,77],{"className":74,"code":75,"language":76,"meta":40,"style":40},"language-python shiki shiki-themes github-light github-dark-high-contrast","import formulas\n\nmodel = formulas.ExcelModel().loads(\"profit_model.xlsx\").finish()\nsolution = model.calculate()\n\ndef cell(ref):\n    return f\"'[profit_model.xlsx]MODEL'!{ref}\"\n\nfor ref, label in [(\"B6\", \"Revenue\"), (\"B9\", \"Operating profit\"), (\"B11\", \"Net profit\"), (\"B12\", \"Net margin\")]:\n    print(f\"{label:17} {solution[cell(ref)].value[0][0]:>12,.4f}\")\n","python",[14,78,79,89,96,114,125,130,143,168,173,231],{"__ignoreMap":40},[44,80,81,85],{"class":46,"line":47},[44,82,84],{"class":83},"s-kum","import",[44,86,88],{"class":87},"skGVy"," formulas\n",[44,90,92],{"class":46,"line":91},2,[44,93,95],{"emptyLinePlaceholder":94},true,"\n",[44,97,99,102,105,108,111],{"class":46,"line":98},3,[44,100,101],{"class":87},"model ",[44,103,104],{"class":83},"=",[44,106,107],{"class":87}," formulas.ExcelModel().loads(",[44,109,110],{"class":54},"\"profit_model.xlsx\"",[44,112,113],{"class":87},").finish()\n",[44,115,117,120,122],{"class":46,"line":116},4,[44,118,119],{"class":87},"solution ",[44,121,104],{"class":83},[44,123,124],{"class":87}," model.calculate()\n",[44,126,128],{"class":46,"line":127},5,[44,129,95],{"emptyLinePlaceholder":94},[44,131,133,136,140],{"class":46,"line":132},6,[44,134,135],{"class":83},"def",[44,137,139],{"class":138},"s_Opv"," cell",[44,141,142],{"class":87},"(ref):\n",[44,144,146,149,152,155,159,162,165],{"class":46,"line":145},7,[44,147,148],{"class":83},"    return",[44,150,151],{"class":83}," f",[44,153,154],{"class":54},"\"'[profit_model.xlsx]MODEL'!",[44,156,158],{"class":157},"sSjpA","{",[44,160,161],{"class":87},"ref",[44,163,164],{"class":157},"}",[44,166,167],{"class":54},"\"\n",[44,169,171],{"class":46,"line":170},8,[44,172,95],{"emptyLinePlaceholder":94},[44,174,176,179,182,185,188,191,194,197,200,203,205,208,210,213,215,218,220,223,225,228],{"class":46,"line":175},9,[44,177,178],{"class":83},"for",[44,180,181],{"class":87}," ref, label ",[44,183,184],{"class":83},"in",[44,186,187],{"class":87}," [(",[44,189,190],{"class":54},"\"B6\"",[44,192,193],{"class":87},", ",[44,195,196],{"class":54},"\"Revenue\"",[44,198,199],{"class":87},"), (",[44,201,202],{"class":54},"\"B9\"",[44,204,193],{"class":87},[44,206,207],{"class":54},"\"Operating profit\"",[44,209,199],{"class":87},[44,211,212],{"class":54},"\"B11\"",[44,214,193],{"class":87},[44,216,217],{"class":54},"\"Net profit\"",[44,219,199],{"class":87},[44,221,222],{"class":54},"\"B12\"",[44,224,193],{"class":87},[44,226,227],{"class":54},"\"Net margin\"",[44,229,230],{"class":87},")]:\n",[44,232,234,238,241,244,247,249,252,255,257,260,263,266,269,271,274,277,279,281],{"class":46,"line":233},10,[44,235,237],{"class":236},"sP0c6","    print",[44,239,240],{"class":87},"(",[44,242,243],{"class":83},"f",[44,245,246],{"class":54},"\"",[44,248,158],{"class":157},[44,250,251],{"class":87},"label",[44,253,254],{"class":83},":17",[44,256,164],{"class":157},[44,258,259],{"class":157}," {",[44,261,262],{"class":87},"solution[cell(ref)].value[",[44,264,265],{"class":236},"0",[44,267,268],{"class":87},"][",[44,270,265],{"class":236},[44,272,273],{"class":87},"]",[44,275,276],{"class":83},":>12,.4f",[44,278,164],{"class":157},[44,280,246],{"class":54},[44,282,283],{"class":87},")\n",[10,285,286,289,290,293,294,297,298,301],{},[14,287,288],{},"loads"," parses the workbook, ",[14,291,292],{},"finish"," builds the dependency graph, and ",[14,295,296],{},"calculate"," evaluates every formula from the stored inputs. The results are keyed by fully qualified cell references — workbook, sheet and cell, with the sheet name in capitals — which is verbose, so a small helper builds the keys. Each value is a small two-dimensional array, because the library treats every reference as a range; ",[14,299,300],{},"[0][0]"," takes the single value. With the stored inputs the model gives revenue of 294,000, operating profit of 43,600 and net profit of 32,700, a net margin of about 11.1%.",[30,303,305],{"id":304},"step-2-check-the-answers-against-a-real-spreadsheet-engine","Step 2 — Check the answers against a real spreadsheet engine",[35,307,309],{"className":37,"code":308,"language":39,"meta":40,"style":40},"soffice --headless --convert-to xlsx --outdir checked profit_model.xlsx > \u002Fdev\u002Fnull 2>&1\npython -c \"\nfrom openpyxl import load_workbook\nws = load_workbook('checked\u002Fprofit_model.xlsx', data_only=True)['Model']\nprint({ws[f'A{r}'].value: ws[f'B{r}'].value for r in (6, 9, 11, 12)})\"\n",[14,310,311,343,353,358,363],{"__ignoreMap":40},[44,312,313,316,319,322,325,328,331,334,337,340],{"class":46,"line":47},[44,314,315],{"class":50},"soffice",[44,317,318],{"class":236}," --headless",[44,320,321],{"class":236}," --convert-to",[44,323,324],{"class":54}," xlsx",[44,326,327],{"class":236}," --outdir",[44,329,330],{"class":54}," checked",[44,332,333],{"class":54}," profit_model.xlsx",[44,335,336],{"class":83}," >",[44,338,339],{"class":54}," \u002Fdev\u002Fnull",[44,341,342],{"class":83}," 2>&1\n",[44,344,345,347,350],{"class":46,"line":91},[44,346,76],{"class":50},[44,348,349],{"class":236}," -c",[44,351,352],{"class":54}," \"\n",[44,354,355],{"class":46,"line":98},[44,356,357],{"class":54},"from openpyxl import load_workbook\n",[44,359,360],{"class":46,"line":116},[44,361,362],{"class":54},"ws = load_workbook('checked\u002Fprofit_model.xlsx', data_only=True)['Model']\n",[44,364,365],{"class":46,"line":127},[44,366,367],{"class":54},"print({ws[f'A{r}'].value: ws[f'B{r}'].value for r in (6, 9, 11, 12)})\"\n",[10,369,370,371,374,375,28],{},"Before relying on a second implementation of Excel's calculation rules, compare it with an established one. LibreOffice recalculates the same workbook and saves the results, which openpyxl reads with ",[14,372,373],{},"data_only=True","; the values match the library's exactly. It is worth repeating this check whenever the model changes, and especially when new functions appear in it, because the library implements Excel functions one by one and its coverage, while broad, is not complete. The recalculation route on its own is described in ",[24,376,378],{"href":377},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Frecalculate-excel-formulas-without-excel-in-python\u002F","Recalculate Excel Formulas Without Excel in Python",[380,381,389,390,389,394,389,398,389,405,389,415,389,422,389,427,389,431,389,437,389,442,389,446,389,451,389,454,389,457,389,461,389,464,389,469,389,474,389,477,389,480,389,484,389,487,389,492,389,498,389,502,389,506,389,510,389,514,389,517,389,520,389,524,389,527,389,530],"svg",{"viewBox":382,"role":383,"ariaLabelledBy":384,"xmlns":387,"style":388},"1 0 755 126","img",[385,386],"fm-compile-t","fm-compile-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:755px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[391,392,393],"title",{"id":385},"From workbook to function",[395,396,397],"desc",{"id":386},"The formulas library parses the workbook and builds a graph of which cells depend on which. Compiling with chosen input and output cells keeps only the part of the graph between them and returns a Python function that takes the inputs and returns the outputs.",[399,400],"rect",{"x":401,"y":265,"width":402,"height":403,"fill":404},"1","755","126","#ffffff",[399,406],{"x":407,"y":408,"width":409,"height":410,"rx":411,"fill":412,"stroke":413,"style":414},"20.0","16.0","115.2","94.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[416,417,421],"text",{"x":418,"y":419,"style":420},"77.6","50.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","profit_model.xlsx",[416,423,426],{"x":418,"y":424,"style":425},"69.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","owned by",[416,428,430],{"x":418,"y":429,"style":425},"87.2","finance",[46,432],{"x1":433,"y1":434,"x2":435,"y2":434,"stroke":436,"style":414},"140.2","63.0","162.2","var(--brand,#5b5cf0)",[438,439],"polygon",{"points":440,"fill":441},"166.2,63.0 157.2,68.0 157.2,58.0","#5b5cf0",[399,443],{"x":444,"y":408,"width":409,"height":410,"rx":411,"fill":445,"stroke":436,"style":414},"171.2","#f0f4ff",[416,447,450],{"x":448,"y":419,"style":449},"228.8","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Parse",[416,452,16],{"x":448,"y":453,"style":449},"67.8",[416,455,456],{"x":448,"y":429,"style":425},"loads + finish",[46,458],{"x1":459,"y1":434,"x2":460,"y2":434,"stroke":436,"style":414},"291.4","313.4",[438,462],{"points":463,"fill":441},"317.4,63.0 308.4,68.0 308.4,58.0",[399,465],{"x":466,"y":408,"width":409,"height":410,"rx":411,"fill":467,"stroke":468,"style":414},"322.4","#fdefd8","var(--gold,#b4740a)",[416,470,473],{"x":471,"y":419,"style":472},"380.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Dependency",[416,475,476],{"x":471,"y":453,"style":472},"graph",[416,478,479],{"x":471,"y":429,"style":425},"cell → cell",[46,481],{"x1":482,"y1":434,"x2":483,"y2":434,"stroke":436,"style":414},"442.6","464.6",[438,485],{"points":486,"fill":441},"468.6,63.0 459.6,68.0 459.6,58.0",[399,488],{"x":489,"y":408,"width":409,"height":410,"rx":411,"fill":490,"stroke":491,"style":414},"473.6","#d9f4f1","var(--teal,#0f9488)",[416,493,497],{"x":494,"y":495,"style":496},"531.2","41.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","compile(inputs,",[416,499,501],{"x":494,"y":500,"style":496},"59.0","outputs)",[416,503,505],{"x":494,"y":504,"style":425},"78.5","keep the path",[416,507,509],{"x":494,"y":508,"style":425},"96.0","between",[46,511],{"x1":512,"y1":434,"x2":513,"y2":434,"stroke":436,"style":414},"593.8","615.8",[438,515],{"points":516,"fill":441},"619.8,63.0 610.8,68.0 610.8,58.0",[399,518],{"x":519,"y":408,"width":409,"height":410,"rx":411,"fill":445,"stroke":436,"style":414},"624.8",[416,521,523],{"x":522,"y":495,"style":449},"682.4","f(units,",[416,525,526],{"x":522,"y":500,"style":449},"price)",[416,528,529],{"x":522,"y":504,"style":425},"a Python",[416,531,532],{"x":522,"y":508,"style":425},"function",[30,534,536],{"id":535},"step-3-compile-the-model-into-a-function","Step 3 — Compile the model into a function",[35,538,540],{"className":74,"code":539,"language":76,"meta":40,"style":40},"profit = model.compile(inputs=[cell(\"B1\"), cell(\"B2\")], outputs=[cell(\"B11\"), cell(\"B12\")])\n\ndef run(units, price):\n    net, margin = profit(units, price)\n    return net.value[0][0], margin.value[0][0]\n\nprint(run(12000, 24.5))\nprint(run(10000, 27.0))\nprint(run(6000, 22.0))\n",[14,541,542,589,593,603,613,638,642,661,677],{"__ignoreMap":40},[44,543,544,547,549,552,556,558,561,564,567,570,573,576,578,580,582,584,586],{"class":46,"line":47},[44,545,546],{"class":87},"profit ",[44,548,104],{"class":83},[44,550,551],{"class":87}," model.compile(",[44,553,555],{"class":554},"sa561","inputs",[44,557,104],{"class":83},[44,559,560],{"class":87},"[cell(",[44,562,563],{"class":54},"\"B1\"",[44,565,566],{"class":87},"), cell(",[44,568,569],{"class":54},"\"B2\"",[44,571,572],{"class":87},")], ",[44,574,575],{"class":554},"outputs",[44,577,104],{"class":83},[44,579,560],{"class":87},[44,581,212],{"class":54},[44,583,566],{"class":87},[44,585,222],{"class":54},[44,587,588],{"class":87},")])\n",[44,590,591],{"class":46,"line":91},[44,592,95],{"emptyLinePlaceholder":94},[44,594,595,597,600],{"class":46,"line":98},[44,596,135],{"class":83},[44,598,599],{"class":138}," run",[44,601,602],{"class":87},"(units, price):\n",[44,604,605,608,610],{"class":46,"line":116},[44,606,607],{"class":87},"    net, margin ",[44,609,104],{"class":83},[44,611,612],{"class":87}," profit(units, price)\n",[44,614,615,617,620,622,624,626,629,631,633,635],{"class":46,"line":127},[44,616,148],{"class":83},[44,618,619],{"class":87}," net.value[",[44,621,265],{"class":236},[44,623,268],{"class":87},[44,625,265],{"class":236},[44,627,628],{"class":87},"], margin.value[",[44,630,265],{"class":236},[44,632,268],{"class":87},[44,634,265],{"class":236},[44,636,637],{"class":87},"]\n",[44,639,640],{"class":46,"line":132},[44,641,95],{"emptyLinePlaceholder":94},[44,643,644,647,650,653,655,658],{"class":46,"line":145},[44,645,646],{"class":236},"print",[44,648,649],{"class":87},"(run(",[44,651,652],{"class":236},"12000",[44,654,193],{"class":87},[44,656,657],{"class":236},"24.5",[44,659,660],{"class":87},"))\n",[44,662,663,665,667,670,672,675],{"class":46,"line":170},[44,664,646],{"class":236},[44,666,649],{"class":87},[44,668,669],{"class":236},"10000",[44,671,193],{"class":87},[44,673,674],{"class":236},"27.0",[44,676,660],{"class":87},[44,678,679,681,683,686,688,691],{"class":46,"line":175},[44,680,646],{"class":236},[44,682,649],{"class":87},[44,684,685],{"class":236},"6000",[44,687,193],{"class":87},[44,689,690],{"class":236},"22.0",[44,692,660],{"class":87},[10,694,695,697,698,701],{},[14,696,21],{}," takes a list of input cells and a list of output cells and returns a callable. The arguments are the input values in the same order; everything not listed as an input — unit cost, fixed costs, tax rate — keeps the value stored in the workbook. The function evaluates only the formulas between the inputs and the outputs. Selling 10,000 units at 27.00 instead of 12,000 at 24.50 raises net profit from 32,700 to 37,500, because the higher price more than makes up for the lost volume. At 6,000 units and 22.00 the business makes an operating loss of 27,200, the ",[14,699,700],{},"MAX(0, …)"," in the tax formula keeps tax at zero, and the net margin comes out at about −20.6%.",[380,703,389,709,389,712,389,715,389,720,389,726,389,730,389,736,389,740,389,744,389,748,389,752,389,756,389,760,389,764,389,767,389,770,389,774,389,776,389,779,389,781,389,784,389,786,389,789,389,792,389,796,389,798,389,802,389,804,389,807,389,809,389,812,389,814,389,817,389,819,389,823,389,825,389,828,389,830,389,833,389,835,389,839,389,841,389,844,389,847,389,851,389,853,389,856,389,858,389,861,389,865,389,869,389,871],{"viewBox":704,"role":383,"ariaLabelledBy":705,"xmlns":387,"style":708},"196 0 368 168",[706,707],"fm-runs-t","fm-runs-d","width:100%;max-width:368px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[391,710,711],{"id":706},"Three calls to the compiled model",[395,713,714],{"id":707},"At 12,000 units and 24.50 the net profit is 32,700 with an 11.1 percent margin. At 10,000 units and 27.00 it is 37,500 with a 13.9 percent margin. At 6,000 units and 22.00 there is a loss of 27,200 with no tax.",[399,716],{"x":717,"y":265,"width":718,"height":719,"fill":404},"196","368","168",[399,721],{"x":722,"y":408,"width":723,"height":724,"rx":265,"fill":412,"stroke":413,"style":725},"212.4","32.0","24.0","stroke-width:1px",[399,727],{"x":728,"y":408,"width":729,"height":724,"rx":265,"fill":412,"stroke":413,"style":725},"244.4","67.6",[416,731,735],{"x":732,"y":733,"style":734},"278.3","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[399,737],{"x":738,"y":408,"width":739,"height":724,"rx":265,"fill":412,"stroke":413,"style":725},"312.1","61.1",[416,741,743],{"x":742,"y":733,"style":734},"342.6","B",[399,745],{"x":746,"y":408,"width":747,"height":724,"rx":265,"fill":412,"stroke":413,"style":725},"373.1","82.5",[416,749,751],{"x":750,"y":733,"style":734},"414.4","C",[399,753],{"x":754,"y":408,"width":755,"height":724,"rx":265,"fill":412,"stroke":413,"style":725},"455.7","91.9",[416,757,759],{"x":758,"y":733,"style":734},"501.6","D",[399,761],{"x":722,"y":762,"width":723,"height":763,"rx":265,"fill":412,"stroke":413,"style":725},"40.0","28.0",[416,765,401],{"x":766,"y":500,"style":734},"228.4",[399,768],{"x":728,"y":762,"width":729,"height":763,"rx":265,"fill":445,"stroke":436,"style":769},"stroke-width:1.5px",[416,771,773],{"x":732,"y":500,"style":772},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Units",[399,775],{"x":738,"y":762,"width":739,"height":763,"rx":265,"fill":445,"stroke":436,"style":769},[416,777,778],{"x":742,"y":500,"style":772},"Price",[399,780],{"x":746,"y":762,"width":747,"height":763,"rx":265,"fill":445,"stroke":436,"style":769},[416,782,783],{"x":750,"y":500,"style":772},"Net profit",[399,785],{"x":754,"y":762,"width":755,"height":763,"rx":265,"fill":445,"stroke":436,"style":769},[416,787,788],{"x":758,"y":500,"style":772},"Net margin",[399,790],{"x":722,"y":791,"width":723,"height":763,"rx":265,"fill":412,"stroke":413,"style":725},"68.0",[416,793,795],{"x":766,"y":794,"style":734},"87.0","2",[399,797],{"x":728,"y":791,"width":729,"height":763,"rx":265,"fill":404,"stroke":413,"style":725},[416,799,801],{"x":732,"y":794,"style":800},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","12,000",[399,803],{"x":738,"y":791,"width":739,"height":763,"rx":265,"fill":404,"stroke":413,"style":725},[416,805,806],{"x":742,"y":794,"style":800},"24.50",[399,808],{"x":746,"y":791,"width":747,"height":763,"rx":265,"fill":404,"stroke":413,"style":725},[416,810,811],{"x":750,"y":794,"style":800},"32,700",[399,813],{"x":754,"y":791,"width":755,"height":763,"rx":265,"fill":404,"stroke":413,"style":725},[416,815,816],{"x":758,"y":794,"style":800},"11.1%",[399,818],{"x":722,"y":508,"width":723,"height":763,"rx":265,"fill":412,"stroke":413,"style":725},[416,820,822],{"x":766,"y":821,"style":734},"115.0","3",[399,824],{"x":728,"y":508,"width":729,"height":763,"rx":265,"fill":404,"stroke":413,"style":725},[416,826,827],{"x":732,"y":821,"style":800},"10,000",[399,829],{"x":738,"y":508,"width":739,"height":763,"rx":265,"fill":404,"stroke":413,"style":725},[416,831,832],{"x":742,"y":821,"style":800},"27.00",[399,834],{"x":746,"y":508,"width":747,"height":763,"rx":265,"fill":490,"stroke":491,"style":769},[416,836,838],{"x":750,"y":821,"style":837},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","37,500",[399,840],{"x":754,"y":508,"width":755,"height":763,"rx":265,"fill":404,"stroke":413,"style":725},[416,842,843],{"x":758,"y":821,"style":800},"13.9%",[399,845],{"x":722,"y":846,"width":723,"height":763,"rx":265,"fill":412,"stroke":413,"style":725},"124.0",[416,848,850],{"x":766,"y":849,"style":734},"143.0","4",[399,852],{"x":728,"y":846,"width":729,"height":763,"rx":265,"fill":404,"stroke":413,"style":725},[416,854,855],{"x":732,"y":849,"style":800},"6,000",[399,857],{"x":738,"y":846,"width":739,"height":763,"rx":265,"fill":404,"stroke":413,"style":725},[416,859,860],{"x":742,"y":849,"style":800},"22.00",[399,862],{"x":746,"y":846,"width":747,"height":763,"rx":265,"fill":863,"stroke":864,"style":769},"#fce9e9","var(--danger,#dc2626)",[416,866,868],{"x":750,"y":849,"style":867},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","−27,200",[399,870],{"x":754,"y":846,"width":755,"height":763,"rx":265,"fill":404,"stroke":413,"style":725},[416,872,873],{"x":758,"y":849,"style":800},"−20.6%",[30,875,877],{"id":876},"step-4-run-two-thousand-scenarios","Step 4 — Run two thousand scenarios",[35,879,881],{"className":74,"code":880,"language":76,"meta":40,"style":40},"import time\nimport numpy as np\n\nrng = np.random.default_rng(42)\nunits = rng.normal(11000, 1500, 2000).round()\nprices = rng.uniform(22.0, 27.0, 2000).round(2)\n\nt0 = time.perf_counter()\nresults = np.array([run(u, p) for u, p in zip(units, prices)])\nelapsed = time.perf_counter() - t0\n\nnet = results[:, 0]\nprint(f\"{len(net)} scenarios in {elapsed:.1f} s\")  # about 1.7 s on the machine used for this page\nprint(f\"median net profit {np.median(net):,.0f}; 5th–95th percentile {np.percentile(net, 5):,.0f} to {np.percentile(net, 95):,.0f}\")\nprint(f\"probability of a loss: {(net \u003C 0).mean():.1%}\")\n",[14,882,883,890,903,907,922,948,975,979,989,1012,1028,1033,1048,1092,1152],{"__ignoreMap":40},[44,884,885,887],{"class":46,"line":47},[44,886,84],{"class":83},[44,888,889],{"class":87}," time\n",[44,891,892,894,897,900],{"class":46,"line":91},[44,893,84],{"class":83},[44,895,896],{"class":87}," numpy ",[44,898,899],{"class":83},"as",[44,901,902],{"class":87}," np\n",[44,904,905],{"class":46,"line":98},[44,906,95],{"emptyLinePlaceholder":94},[44,908,909,912,914,917,920],{"class":46,"line":116},[44,910,911],{"class":87},"rng ",[44,913,104],{"class":83},[44,915,916],{"class":87}," np.random.default_rng(",[44,918,919],{"class":236},"42",[44,921,283],{"class":87},[44,923,924,927,929,932,935,937,940,942,945],{"class":46,"line":127},[44,925,926],{"class":87},"units ",[44,928,104],{"class":83},[44,930,931],{"class":87}," rng.normal(",[44,933,934],{"class":236},"11000",[44,936,193],{"class":87},[44,938,939],{"class":236},"1500",[44,941,193],{"class":87},[44,943,944],{"class":236},"2000",[44,946,947],{"class":87},").round()\n",[44,949,950,953,955,958,960,962,964,966,968,971,973],{"class":46,"line":132},[44,951,952],{"class":87},"prices ",[44,954,104],{"class":83},[44,956,957],{"class":87}," rng.uniform(",[44,959,690],{"class":236},[44,961,193],{"class":87},[44,963,674],{"class":236},[44,965,193],{"class":87},[44,967,944],{"class":236},[44,969,970],{"class":87},").round(",[44,972,795],{"class":236},[44,974,283],{"class":87},[44,976,977],{"class":46,"line":145},[44,978,95],{"emptyLinePlaceholder":94},[44,980,981,984,986],{"class":46,"line":170},[44,982,983],{"class":87},"t0 ",[44,985,104],{"class":83},[44,987,988],{"class":87}," time.perf_counter()\n",[44,990,991,994,996,999,1001,1004,1006,1009],{"class":46,"line":175},[44,992,993],{"class":87},"results ",[44,995,104],{"class":83},[44,997,998],{"class":87}," np.array([run(u, p) ",[44,1000,178],{"class":83},[44,1002,1003],{"class":87}," u, p ",[44,1005,184],{"class":83},[44,1007,1008],{"class":236}," zip",[44,1010,1011],{"class":87},"(units, prices)])\n",[44,1013,1014,1017,1019,1022,1025],{"class":46,"line":233},[44,1015,1016],{"class":87},"elapsed ",[44,1018,104],{"class":83},[44,1020,1021],{"class":87}," time.perf_counter() ",[44,1023,1024],{"class":83},"-",[44,1026,1027],{"class":87}," t0\n",[44,1029,1031],{"class":46,"line":1030},11,[44,1032,95],{"emptyLinePlaceholder":94},[44,1034,1036,1039,1041,1044,1046],{"class":46,"line":1035},12,[44,1037,1038],{"class":87},"net ",[44,1040,104],{"class":83},[44,1042,1043],{"class":87}," results[:, ",[44,1045,265],{"class":236},[44,1047,637],{"class":87},[44,1049,1051,1053,1055,1057,1059,1061,1064,1067,1069,1072,1074,1077,1080,1082,1085,1088],{"class":46,"line":1050},13,[44,1052,646],{"class":236},[44,1054,240],{"class":87},[44,1056,243],{"class":83},[44,1058,246],{"class":54},[44,1060,158],{"class":157},[44,1062,1063],{"class":236},"len",[44,1065,1066],{"class":87},"(net)",[44,1068,164],{"class":157},[44,1070,1071],{"class":54}," scenarios in ",[44,1073,158],{"class":157},[44,1075,1076],{"class":87},"elapsed",[44,1078,1079],{"class":83},":.1f",[44,1081,164],{"class":157},[44,1083,1084],{"class":54}," s\"",[44,1086,1087],{"class":87},")  ",[44,1089,1091],{"class":1090},"s-wDw","# about 1.7 s on the machine used for this page\n",[44,1093,1095,1097,1099,1101,1104,1106,1109,1112,1114,1117,1119,1122,1125,1128,1130,1132,1135,1137,1139,1142,1144,1146,1148,1150],{"class":46,"line":1094},14,[44,1096,646],{"class":236},[44,1098,240],{"class":87},[44,1100,243],{"class":83},[44,1102,1103],{"class":54},"\"median net profit ",[44,1105,158],{"class":157},[44,1107,1108],{"class":87},"np.median(net)",[44,1110,1111],{"class":83},":,.0f",[44,1113,164],{"class":157},[44,1115,1116],{"class":54},"; 5th–95th percentile ",[44,1118,158],{"class":157},[44,1120,1121],{"class":87},"np.percentile(net, ",[44,1123,1124],{"class":236},"5",[44,1126,1127],{"class":87},")",[44,1129,1111],{"class":83},[44,1131,164],{"class":157},[44,1133,1134],{"class":54}," to ",[44,1136,158],{"class":157},[44,1138,1121],{"class":87},[44,1140,1141],{"class":236},"95",[44,1143,1127],{"class":87},[44,1145,1111],{"class":83},[44,1147,164],{"class":157},[44,1149,246],{"class":54},[44,1151,283],{"class":87},[44,1153,1155,1157,1159,1161,1164,1166,1169,1172,1175,1178,1181,1183,1185],{"class":46,"line":1154},15,[44,1156,646],{"class":236},[44,1158,240],{"class":87},[44,1160,243],{"class":83},[44,1162,1163],{"class":54},"\"probability of a loss: ",[44,1165,158],{"class":157},[44,1167,1168],{"class":87},"(net ",[44,1170,1171],{"class":83},"\u003C",[44,1173,1174],{"class":236}," 0",[44,1176,1177],{"class":87},").mean()",[44,1179,1180],{"class":83},":.1%",[44,1182,164],{"class":157},[44,1184,246],{"class":54},[44,1186,283],{"class":87},[10,1188,1189],{},"With the model as a function, a simulation is a loop. Units are drawn from a normal distribution around 11,000 and prices uniformly between 22 and 27, and each pair goes through the compiled model. Here two thousand runs took under two seconds — much less than recalculating two thousand copies of the workbook with LibreOffice, where each recalculation means writing a file, starting a conversion and reading the result back. The output is the distribution of net profit: its median, the range covering 90% of scenarios, and the share of scenarios that make a loss — a median of about 23,800, a 90% range from roughly 1,100 to 51,600, and a loss in about 4% of scenarios. The same pattern gives a sensitivity table — vary one input at a time across a range and record the output — or feeds an optimiser looking for the price that maximises profit. Because the logic is still the workbook's, finance can change a formula, save the file, and the next simulation uses the new logic without any change to the Python.",[30,1191,1193],{"id":1192},"step-5-detect-formulas-the-library-cannot-evaluate","Step 5 — Detect formulas the library cannot evaluate",[35,1195,1197],{"className":74,"code":1196,"language":76,"meta":40,"style":40},"from openpyxl import Workbook\n\ncheck = Workbook()\ncs = check.active\ncs[\"A1\"] = '=_xlfn.XLOOKUP(2,{1,2,3},{10,20,30})'\ncs[\"A2\"] = '=WEBSERVICE(\"https:\u002F\u002Fexample.com\u002Frate\")'\ncs[\"A3\"] = '=_xlfn.STOCKHISTORY(\"MSFT\",1)'\ncheck.save(\"function_check.xlsx\")\n\nresults = formulas.ExcelModel().loads(\"function_check.xlsx\").finish().calculate()\nfor ref in (\"A1\", \"A2\", \"A3\"):\n    v = results[f\"'[function_check.xlsx]SHEET'!{ref}\"].value[0][0]\n    flag = \"UNSUPPORTED\" if str(v) == \"#NAME?\" else \"ok\"\n    print(f\"{ref:4} {v!s:8} {flag}\")\n",[14,1198,1199,1212,1216,1226,1236,1252,1266,1280,1290,1294,1307,1332,1366,1397],{"__ignoreMap":40},[44,1200,1201,1204,1207,1209],{"class":46,"line":47},[44,1202,1203],{"class":83},"from",[44,1205,1206],{"class":87}," openpyxl ",[44,1208,84],{"class":83},[44,1210,1211],{"class":87}," Workbook\n",[44,1213,1214],{"class":46,"line":91},[44,1215,95],{"emptyLinePlaceholder":94},[44,1217,1218,1221,1223],{"class":46,"line":98},[44,1219,1220],{"class":87},"check ",[44,1222,104],{"class":83},[44,1224,1225],{"class":87}," Workbook()\n",[44,1227,1228,1231,1233],{"class":46,"line":116},[44,1229,1230],{"class":87},"cs ",[44,1232,104],{"class":83},[44,1234,1235],{"class":87}," check.active\n",[44,1237,1238,1241,1244,1247,1249],{"class":46,"line":127},[44,1239,1240],{"class":87},"cs[",[44,1242,1243],{"class":54},"\"A1\"",[44,1245,1246],{"class":87},"] ",[44,1248,104],{"class":83},[44,1250,1251],{"class":54}," '=_xlfn.XLOOKUP(2,{1,2,3},{10,20,30})'\n",[44,1253,1254,1256,1259,1261,1263],{"class":46,"line":132},[44,1255,1240],{"class":87},[44,1257,1258],{"class":54},"\"A2\"",[44,1260,1246],{"class":87},[44,1262,104],{"class":83},[44,1264,1265],{"class":54}," '=WEBSERVICE(\"https:\u002F\u002Fexample.com\u002Frate\")'\n",[44,1267,1268,1270,1273,1275,1277],{"class":46,"line":145},[44,1269,1240],{"class":87},[44,1271,1272],{"class":54},"\"A3\"",[44,1274,1246],{"class":87},[44,1276,104],{"class":83},[44,1278,1279],{"class":54}," '=_xlfn.STOCKHISTORY(\"MSFT\",1)'\n",[44,1281,1282,1285,1288],{"class":46,"line":170},[44,1283,1284],{"class":87},"check.save(",[44,1286,1287],{"class":54},"\"function_check.xlsx\"",[44,1289,283],{"class":87},[44,1291,1292],{"class":46,"line":175},[44,1293,95],{"emptyLinePlaceholder":94},[44,1295,1296,1298,1300,1302,1304],{"class":46,"line":233},[44,1297,993],{"class":87},[44,1299,104],{"class":83},[44,1301,107],{"class":87},[44,1303,1287],{"class":54},[44,1305,1306],{"class":87},").finish().calculate()\n",[44,1308,1309,1311,1314,1316,1319,1321,1323,1325,1327,1329],{"class":46,"line":1030},[44,1310,178],{"class":83},[44,1312,1313],{"class":87}," ref ",[44,1315,184],{"class":83},[44,1317,1318],{"class":87}," (",[44,1320,1243],{"class":54},[44,1322,193],{"class":87},[44,1324,1258],{"class":54},[44,1326,193],{"class":87},[44,1328,1272],{"class":54},[44,1330,1331],{"class":87},"):\n",[44,1333,1334,1337,1339,1342,1344,1347,1349,1351,1353,1355,1358,1360,1362,1364],{"class":46,"line":1035},[44,1335,1336],{"class":87},"    v ",[44,1338,104],{"class":83},[44,1340,1341],{"class":87}," results[",[44,1343,243],{"class":83},[44,1345,1346],{"class":54},"\"'[function_check.xlsx]SHEET'!",[44,1348,158],{"class":157},[44,1350,161],{"class":87},[44,1352,164],{"class":157},[44,1354,246],{"class":54},[44,1356,1357],{"class":87},"].value[",[44,1359,265],{"class":236},[44,1361,268],{"class":87},[44,1363,265],{"class":236},[44,1365,637],{"class":87},[44,1367,1368,1371,1373,1376,1379,1382,1385,1388,1391,1394],{"class":46,"line":1050},[44,1369,1370],{"class":87},"    flag ",[44,1372,104],{"class":83},[44,1374,1375],{"class":54}," \"UNSUPPORTED\"",[44,1377,1378],{"class":83}," if",[44,1380,1381],{"class":236}," str",[44,1383,1384],{"class":87},"(v) ",[44,1386,1387],{"class":83},"==",[44,1389,1390],{"class":54}," \"#NAME?\"",[44,1392,1393],{"class":83}," else",[44,1395,1396],{"class":54}," \"ok\"\n",[44,1398,1399,1401,1403,1405,1407,1409,1411,1414,1416,1418,1421,1424,1426,1428,1431,1433,1435],{"class":46,"line":1094},[44,1400,237],{"class":236},[44,1402,240],{"class":87},[44,1404,243],{"class":83},[44,1406,246],{"class":54},[44,1408,158],{"class":157},[44,1410,161],{"class":87},[44,1412,1413],{"class":83},":4",[44,1415,164],{"class":157},[44,1417,259],{"class":157},[44,1419,1420],{"class":87},"v",[44,1422,1423],{"class":83},"!s:8",[44,1425,164],{"class":157},[44,1427,259],{"class":157},[44,1429,1430],{"class":87},"flag",[44,1432,164],{"class":157},[44,1434,246],{"class":54},[44,1436,283],{"class":87},[10,1438,1439,1440,1443,1444,193,1447,1450,1451,1454,1455,193,1458,1461,1462,1465,1466,1468],{},"When the library meets a function it does not implement, it does not raise an error — it returns ",[14,1441,1442],{},"#NAME?",", the same error Excel shows for an unknown function, and that error then flows into every dependent cell. Modern functions such as ",[14,1445,1446],{},"XLOOKUP",[14,1448,1449],{},"LET"," and ",[14,1452,1453],{},"TEXTJOIN"," are supported, but functions that reach outside the workbook, such as ",[14,1456,1457],{},"WEBSERVICE",[14,1459,1460],{},"STOCKHISTORY"," and the cube functions, are not, and neither are a number of rarely used statistical and engineering functions. Scanning the results of a first ",[14,1463,1464],{},"calculate()"," for ",[14,1467,1442],{}," — or any error value — before compiling is a cheap guard against a model that silently returns errors for every scenario.",[30,1470,1472],{"id":1471},"when-this-is-the-right-tool","When this is the right tool",[10,1474,1475,1476,1479,1480,1484,1485,1489],{},"Compiling suits models that are mostly arithmetic, lookups and logic, that finance wants to keep owning in Excel, and that need to be run far more often than a person would recalculate them: simulations, sensitivity tables, optimisation, or a web form that prices a quote. It suits less well models full of external data connections, macros or unsupported functions, and very large models where the first ",[14,1477,1478],{},"finish()"," is slow. When Python will own the logic from now on, rewriting the calculation in pandas is clearer still; the formula equivalents in ",[24,1481,1483],{"href":1482},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002F","Excel Formula Equivalents in pandas"," show how. And when the question is a handful of named scenarios rather than thousands, recalculating copies with LibreOffice, as in ",[24,1486,1488],{"href":1487},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Frun-what-if-scenarios-on-an-excel-model-with-python\u002F","Run What-If Scenarios on an Excel Model with Python",", is simpler and uses a full spreadsheet engine.",[380,1491,389,1497,389,1500,389,1503,389,1507,389,1512,389,1518,389,1523,389,1527,389,1531,389,1535,389,1539,389,1542,389,1547,389,1551,389,1554,389,1557,389,1560],{"viewBox":1492,"role":383,"ariaLabelledBy":1493,"xmlns":387,"style":1496},"4 0 752 199",[1494,1495],"fm-choice-t","fm-choice-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[391,1498,1499],{"id":1494},"Compile with formulas or recalculate with LibreOffice",[395,1501,1502],{"id":1495},"Compiling the model with the formulas library suits thousands of runs of a model built from supported functions. Recalculating copies with LibreOffice suits a few named scenarios and models that use functions the library lacks.",[399,1504],{"x":850,"y":265,"width":1505,"height":1506,"fill":404},"752","199",[399,1508],{"x":407,"y":408,"width":1509,"height":1510,"rx":1511,"fill":490,"stroke":491,"style":414},"351.0","167.0","14",[416,1513,1517],{"x":1514,"y":1515,"style":1516},"195.5","44.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Compile with formulas",[46,1519],{"x1":1520,"y1":1521,"x2":1522,"y2":1521,"stroke":491,"style":725},"36.0","57.0","355.0",[416,1524,1526],{"x":1514,"y":1525,"style":800},"77.0","thousands of runs in seconds",[416,1528,1530],{"x":1514,"y":1529,"style":800},"103.0","in-process, no files written",[416,1532,1534],{"x":1514,"y":1533,"style":800},"129.0","supported functions only",[416,1536,1538],{"x":1514,"y":1537,"style":800},"155.0","check #NAME? first",[399,1540],{"x":1541,"y":408,"width":1509,"height":1510,"rx":1511,"fill":445,"stroke":436,"style":414},"389.0",[416,1543,1546],{"x":1544,"y":1515,"style":1545},"564.5","font-size:14px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Recalculate with LibreOffice",[46,1548],{"x1":1549,"y1":1521,"x2":1550,"y2":1521,"stroke":436,"style":725},"405.0","724.0",[416,1552,1553],{"x":1544,"y":1525,"style":800},"a few named scenarios",[416,1555,1556],{"x":1544,"y":1529,"style":800},"full spreadsheet engine",[416,1558,1559],{"x":1544,"y":1533,"style":800},"one file per scenario",[416,1561,1562],{"x":1544,"y":1537,"style":800},"seconds per run",[30,1564,1566],{"id":1565},"common-pitfalls","Common pitfalls",[1568,1569,1570,1582,1591,1597,1606],"ul",{},[1571,1572,1573,1577,1578,1581],"li",{},[1574,1575,1576],"strong",{},"Short cell references."," Keys are fully qualified: ",[14,1579,1580],{},"'[book.xlsx]SHEET'!B11",", sheet in capitals.",[1571,1583,1584,1587,1588,1590],{},[1574,1585,1586],{},"Forgetting the array."," Each value is a 2-D range; take ",[14,1589,300],{}," for a single cell.",[1571,1592,1593,1596],{},[1574,1594,1595],{},"Trusting it blindly."," Compare a run with LibreOffice or Excel first.",[1571,1598,1599,1602,1603,1605],{},[1574,1600,1601],{},"Unsupported functions."," They return ",[14,1604,1442],{}," silently; scan for error values.",[1571,1607,1608,1611,1612,1614],{},[1574,1609,1610],{},"Inputs not listed."," Cells not passed to ",[14,1613,21],{}," keep their stored values.",[30,1616,1618],{"id":1617},"conclusion","Conclusion",[10,1620,1621,1622,1625,1626,1628],{},"To call an Excel model from Python many times, load it with ",[14,1623,1624],{},"formulas.ExcelModel().loads(path).finish()",", confirm one calculation against LibreOffice, and compile it with the input and output cells you need into a function. That function runs thousands of scenarios in seconds while the logic stays in the workbook finance maintains. Check first that no cell evaluates to ",[14,1627,1442],{},", since unsupported functions fail silently.",[30,1630,1632],{"id":1631},"frequently-asked-questions","Frequently asked questions",[10,1634,1635,1638,1639,1641],{},[1574,1636,1637],{},"Can Python calculate Excel formulas without Excel?","\nYes; the ",[14,1640,16],{}," library parses and evaluates workbook formulas in pure Python, and LibreOffice can recalculate a workbook headlessly.",[10,1643,1644,1647,1648,1651],{},[1574,1645,1646],{},"How do I run an Excel model with different inputs from Python?","\nCompile it with ",[14,1649,1650],{},"model.compile(inputs=[...], outputs=[...])"," and call the returned function with the input values.",[10,1653,1654,1657,1658,1660],{},[1574,1655,1656],{},"What happens if the library does not support a function?","\nThe cell evaluates to ",[14,1659,1442],{},", which then spreads to dependent cells; scan the results for error values.",[10,1662,1663,1666],{},[1574,1664,1665],{},"Is compiling faster than recalculating with LibreOffice?","\nFor many runs, yes: the compiled function evaluates in-process, while each LibreOffice recalculation writes, converts and reads a file.",[30,1668,1670],{"id":1669},"related","Related",[1568,1672,1673,1679,1684,1689],{},[1571,1674,1675,1676,1678],{},"Up one level: ",[24,1677,27],{"href":26}," — formulas from Python.",[1571,1680,1681,1683],{},[24,1682,378],{"href":377}," — one-off recalculation.",[1571,1685,1686,1688],{},[24,1687,1488],{"href":1487}," — named scenarios with LibreOffice.",[1571,1690,1691,1695],{},[24,1692,1694],{"href":1693},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fmap-formula-dependencies-in-a-workbook-with-python\u002F","Map Formula Dependencies in a Workbook with Python"," — understanding the graph.",[1697,1698,1699],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}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}",{"title":40,"searchDepth":91,"depth":91,"links":1701},[1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712],{"id":32,"depth":91,"text":33},{"id":70,"depth":91,"text":71},{"id":304,"depth":91,"text":305},{"id":535,"depth":91,"text":536},{"id":876,"depth":91,"text":877},{"id":1192,"depth":91,"text":1193},{"id":1471,"depth":91,"text":1472},{"id":1565,"depth":91,"text":1566},{"id":1617,"depth":91,"text":1618},{"id":1631,"depth":91,"text":1632},{"id":1669,"depth":91,"text":1670},"2026-10-02","Keep a profit model in Excel where finance owns it, and call it from Python like a function: compile the workbook's formulas with the formulas library into a callable that takes the input cells and returns the output cells, check its answers against LibreOffice, run two thousand Monte Carlo scenarios in a few seconds, and catch functions the library cannot evaluate before trusting the results.","md",[1717,1719,1721,1723],{"q":1637,"a":1718},"Yes; the formulas library parses and evaluates workbook formulas in pure Python, and LibreOffice can recalculate a workbook headlessly.",{"q":1646,"a":1720},"Compile it with model.compile(inputs=[...], outputs=[...]) and call the returned function with the input values.",{"q":1656,"a":1722},"The cell evaluates to #NAME?, which then spreads to dependent cells; scan the results for error values.",{"q":1665,"a":1724},"For many runs, yes: the compiled function evaluates in-process, while each LibreOffice recalculation writes, converts and reads a file.","excel model as python function",null,{"breadcrumb":1728},[1729,1732,1735],{"name":1730,"item":1731},"Home","\u002F",{"name":1733,"item":1734},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":27,"item":26},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fturn-an-excel-model-into-a-python-function",{"title":1738,"description":1739},"Turn an Excel Model into a Python Function with formulas","Compile an Excel model into a Python function with the formulas library: choose input and output cells, check against LibreOffice, run Monte Carlo scenarios and detect unsupported functions.","turn-an-excel-model-into-a-python-function","getting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fturn-an-excel-model-into-a-python-function\u002Findex",[1743,1744],"excel-formulas","formula-equivalents","how-to","ZUMUov5QOWYev0NQZhWwq-L2f0oj2DkyVMz0aqFzQ2s",[1748,1751],{"title":1488,"path":1749,"stem":1750,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Frun-what-if-scenarios-on-an-excel-model-with-python","getting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Frun-what-if-scenarios-on-an-excel-model-with-python\u002Findex",{"title":1752,"path":1753,"stem":1754,"children":-1},"Use Structured Table References in Excel Formulas from Python","\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fuse-structured-table-references-in-formulas-from-python","getting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Fuse-structured-table-references-in-formulas-from-python\u002Findex",[1756,1758],{"path":1757,"title":1733},"\u002Fgetting-started-with-python-excel-automation",{"path":1759,"title":27},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python",[],[1762,1765,1769,1772,1775,1778],{"path":1763,"title":1764,"kind":1745},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fbuilding-excel-reports-with-xlsxwriter\u002Fstore-formula-results-with-xlsxwriter","Store Formula Results with xlsxwriter So Every Viewer Shows Values",{"path":1766,"title":1767,"kind":1768},"\u002Fadvanced-data-transformation-and-cleaning\u002Fapplying-conditional-formatting-with-openpyxl\u002Fhighlight-cells-with-formula-errors-using-conditional-formatting","Highlight Cells with Formula Errors Using Conditional Formatting","fix",{"path":1770,"title":1771,"kind":1745},"\u002Fadvanced-data-transformation-and-cleaning\u002Fexcel-formula-equivalents-in-pandas\u002Fgoal-seek-in-python-instead-of-excel","Goal Seek in Python Instead of Excel",{"path":1773,"title":1774,"kind":1745},"\u002Fadvanced-data-transformation-and-cleaning\u002Fvalidating-excel-data-with-python\u002Fcheck-that-totals-in-a-report-add-up","Check That Totals in a Report Add Up",{"path":1776,"title":1777,"kind":1745},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fcall-excel-worksheet-functions-through-microsoft-graph","Call Excel Worksheet Functions Through Microsoft Graph",{"path":1779,"title":1780,"kind":1745},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Frecalculate-excel-online-workbooks-with-graph-sessions","Recalculate Excel Online Workbooks with Graph Sessions",1790972075828]