[{"data":1,"prerenderedAt":1862},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fcall-python-from-excel-with-xlwings-udf":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fcall-python-from-excel-with-xlwings-udf":1854},{"id":4,"title":5,"body":6,"dateModified":1831,"datePublished":1831,"description":1832,"extension":1833,"faq":1834,"meta":1845,"navigation":324,"path":1846,"seo":1847,"slug":1850,"stem":1851,"type":1852,"__hash__":1853},"docs\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fcall-python-from-excel-with-xlwings-udf\u002Findex.md","Call Python from Excel with an xlwings UDF",{"type":7,"value":8,"toc":1818},"minimark",[9,24,170,175,211,227,233,237,240,263,280,284,290,424,431,438,450,454,583,590,862,868,874,944,948,951,1207,1219,1223,1226,1351,1358,1503,1510,1514,1631,1635,1638,1644,1650,1664,1667,1715,1718,1722,1733,1737,1743,1752,1758,1764,1774,1778,1814],[10,11,12,13,17,18,23],"p",{},"Most Python-and-Excel automation runs one way: Python produces a file, somebody opens it. A user-defined function inverts that. You write a Python function, and a spreadsheet user types ",[14,15,16],"code",{},"=REVENUE_FORECAST(B2:B13, 0.04)"," into a cell and gets the result — with your libraries, your logic, and no VBA. xlwings makes this a decorator and an add-in click. This guide covers the setup, the argument converters that make it pleasant, and the constraints worth knowing before you commit to the approach. It builds on ",[19,20,22],"a",{"href":21},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002F","Automating Excel with xlwings: the Basics",".",[25,26,35,36,35,40,35,44,35,51,35,61,35,68,35,73,35,77,35,82,35,88,35,93,35,101,35,106,35,110,35,115,35,120,35,124,35,128,35,134,35,138,35,143,35,147,35,151,35,156,35,161,35,165],"svg",{"viewBox":27,"role":28,"ariaLabel":29,"ariaLabelledBy":30,"xmlns":33,"style":34},"0 0 800 244","img","A UDF call crossing from Excel to Python: the cell formula passes a range to the xlwings add-in, which calls the decorated Python function and returns the result back into the cell.",[31,32],"udf-t","udf-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:800px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[37,38,39],"title",{"id":31},"What happens when a cell calls a Python function",[41,42,43],"desc",{"id":32},"A worksheet cell containing a formula calls the xlwings add-in, which crosses the process boundary via COM into a Python interpreter. There the decorated function runs with the range converted to a DataFrame, and its return value is converted back and written into the calling cell or spilled across a range. The round trip is the reason a function should take and return whole ranges rather than being called once per row.",[45,46],"rect",{"x":47,"y":47,"width":48,"height":49,"fill":50},"0","800","244","#ffffff",[45,52],{"x":53,"y":54,"width":55,"height":56,"rx":57,"fill":58,"stroke":59,"style":60},"14","70","184","94","13","#d9f4f1","var(--teal,#0f9488)","stroke-width:2px",[62,63,67],"text",{"x":64,"y":65,"style":66},"106","54","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","in the worksheet",[62,69,72],{"x":64,"y":70,"style":71},"102","font-size:11px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","=FORECAST_TOTAL(",[62,74,76],{"x":64,"y":75,"style":71},"122","B2:C13, 0.04)",[62,78,81],{"x":64,"y":79,"style":80},"146","font-size:10px;fill:var(--muted,#5b6780);text-anchor:middle","an ordinary formula",[83,84],"line",{"x1":85,"y1":86,"x2":87,"y2":86,"stroke":59,"style":60},"198","104","230",[89,90],"polygon",{"points":91,"fill":92},"238,104 226,98 226,110","#0f9488",[45,94],{"x":95,"y":96,"width":97,"height":98,"rx":57,"fill":99,"stroke":100,"style":60},"246","60","176","114","#fdefd8","var(--gold,#b4740a)",[62,102,105],{"x":103,"y":104,"style":66},"334","44","the boundary",[62,107,109],{"x":103,"y":56,"style":108},"font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","xlwings add-in",[62,111,114],{"x":103,"y":112,"style":113},"118","font-size:10.5px;fill:var(--text,#172033);text-anchor:middle","COM · Windows only",[62,116,119],{"x":103,"y":117,"style":118},"142","font-size:10.5px;fill:var(--accent-ink,#be185d);text-anchor:middle","one round trip per call",[83,121],{"x1":122,"y1":86,"x2":123,"y2":86,"stroke":100,"style":60},"422","454",[89,125],{"points":126,"fill":127},"462,104 450,98 450,110","#b4740a",[45,129],{"x":130,"y":96,"width":131,"height":98,"rx":57,"fill":132,"stroke":133,"style":60},"470","314","#ebebfd","var(--brand,#5b5cf0)",[62,135,137],{"x":136,"y":104,"style":66},"627","in the Python process",[62,139,142],{"x":136,"y":140,"style":141},"88","font-size:11px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","@xw.func",[62,144,146],{"x":136,"y":145,"style":141},"110","@xw.arg(\"data\", pd.DataFrame)",[62,148,150],{"x":136,"y":149,"style":113},"134","the range arrives as a DataFrame",[62,152,155],{"x":136,"y":153,"style":154},"156","font-size:10.5px;fill:var(--muted,#5b6780);text-anchor:middle","pandas, numpy, your own modules",[157,158],"path",{"d":159,"fill":160,"stroke":133,"style":60},"M627 174 V 200 H 106 V 172","none",[89,162],{"points":163,"fill":164},"106,164 99,176 113,176","#5b5cf0",[62,166,169],{"x":167,"y":168,"style":141},"366","220","the return value spills back into the sheet",[171,172,174],"h2",{"id":173},"prerequisites","Prerequisites",[176,177,182],"pre",{"className":178,"code":179,"language":180,"meta":181,"style":181},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install xlwings\nxlwings addin install\n","bash","",[14,183,184,199],{"__ignoreMap":181},[185,186,188,192,196],"span",{"class":83,"line":187},1,[185,189,191],{"class":190},"sMTad","pip",[185,193,195],{"class":194},"srMev"," install",[185,197,198],{"class":194}," xlwings\n",[185,200,202,205,208],{"class":83,"line":201},2,[185,203,204],{"class":190},"xlwings",[185,206,207],{"class":194}," addin",[185,209,210],{"class":194}," install\n",[10,212,213,217,218,221,222,226],{},[214,215,216],"strong",{},"Windows with Excel installed."," UDFs use COM automation, which exists only in Excel for Windows — there is no macOS or Linux equivalent. If you are producing files on a server, this is the wrong tool entirely; write ",[14,219,220],{},".xlsx"," with ",[19,223,225],{"href":224},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fopenpyxl-vs-xlsxwriter-vs-pandas-excelwriter\u002F","openpyxl or xlsxwriter"," instead.",[10,228,229,230,23],{},"Then, once per machine, enable trust for the VBA object model: Excel → File → Options → Trust Center → Trust Center Settings → Macro Settings → tick \"Trust access to the VBA project object model\". Without it the add-in cannot register your functions and every call returns ",[14,231,232],{},"#NAME?",[171,234,236],{"id":235},"step-1-create-the-project","Step 1 — Create the project",[10,238,239],{},"xlwings scaffolds a matched pair — a workbook and a Python module with the same stem:",[176,241,243],{"className":178,"code":242,"language":180,"meta":181,"style":181},"xlwings quickstart forecast_tools\ncd forecast_tools\n",[14,244,245,255],{"__ignoreMap":181},[185,246,247,249,252],{"class":83,"line":187},[185,248,204],{"class":190},[185,250,251],{"class":194}," quickstart",[185,253,254],{"class":194}," forecast_tools\n",[185,256,257,261],{"class":83,"line":201},[185,258,260],{"class":259},"sP0c6","cd",[185,262,254],{"class":194},[10,264,265,266,269,270,273,274,277,278,23],{},"That gives you ",[14,267,268],{},"forecast_tools.xlsm"," and ",[14,271,272],{},"forecast_tools.py",". ",[214,275,276],{},"The names must match",": the add-in looks for a module named after the workbook. Renaming one without the other is the second most common cause of ",[14,279,232],{},[171,281,283],{"id":282},"step-2-write-the-function","Step 2 — Write the function",[10,285,286,287,289],{},"Decorate a plain Python function with ",[14,288,142],{}," and it becomes a worksheet formula:",[176,291,295],{"className":292,"code":293,"language":294,"meta":181,"style":181},"language-python shiki shiki-themes github-light github-dark-high-contrast","# forecast_tools.py\nimport xlwings as xw\n\n@xw.func\ndef growth_rate(first, last, periods):\n    \"\"\"Compound growth rate between two values over N periods.\"\"\"\n    if first \u003C= 0 or periods \u003C= 0:\n        return \"#VALUE! first and periods must be positive\"\n    return (last \u002F first) ** (1 \u002F periods) - 1\n","python",[14,296,297,303,319,326,333,345,351,379,388],{"__ignoreMap":181},[185,298,299],{"class":83,"line":187},[185,300,302],{"class":301},"s-wDw","# forecast_tools.py\n",[185,304,305,309,313,316],{"class":83,"line":201},[185,306,308],{"class":307},"s-kum","import",[185,310,312],{"class":311},"skGVy"," xlwings ",[185,314,315],{"class":307},"as",[185,317,318],{"class":311}," xw\n",[185,320,322],{"class":83,"line":321},3,[185,323,325],{"emptyLinePlaceholder":324},true,"\n",[185,327,329],{"class":83,"line":328},4,[185,330,332],{"class":331},"s_Opv","@xw.func\n",[185,334,336,339,342],{"class":83,"line":335},5,[185,337,338],{"class":307},"def",[185,340,341],{"class":331}," growth_rate",[185,343,344],{"class":311},"(first, last, periods):\n",[185,346,348],{"class":83,"line":347},6,[185,349,350],{"class":194},"    \"\"\"Compound growth rate between two values over N periods.\"\"\"\n",[185,352,354,357,360,363,366,369,372,374,376],{"class":83,"line":353},7,[185,355,356],{"class":307},"    if",[185,358,359],{"class":311}," first ",[185,361,362],{"class":307},"\u003C=",[185,364,365],{"class":259}," 0",[185,367,368],{"class":307}," or",[185,370,371],{"class":311}," periods ",[185,373,362],{"class":307},[185,375,365],{"class":259},[185,377,378],{"class":311},":\n",[185,380,382,385],{"class":83,"line":381},8,[185,383,384],{"class":307},"        return",[185,386,387],{"class":194}," \"#VALUE! first and periods must be positive\"\n",[185,389,391,394,397,400,403,406,409,412,415,418,421],{"class":83,"line":390},9,[185,392,393],{"class":307},"    return",[185,395,396],{"class":311}," (last ",[185,398,399],{"class":307},"\u002F",[185,401,402],{"class":311}," first) ",[185,404,405],{"class":307},"**",[185,407,408],{"class":311}," (",[185,410,411],{"class":259},"1",[185,413,414],{"class":307}," \u002F",[185,416,417],{"class":311}," periods) ",[185,419,420],{"class":307},"-",[185,422,423],{"class":259}," 1\n",[10,425,426,427,430],{},"In Excel, click ",[214,428,429],{},"Import Functions"," on the xlwings ribbon tab, then use it like anything built in:",[176,432,436],{"className":433,"code":435,"language":62,"meta":181},[434],"language-text","=growth_rate(B2, B13, 11)\n",[14,437,435],{"__ignoreMap":181},[10,439,440,441,445,446,449],{},"Returning an error ",[442,443,444],"em",{},"string"," rather than raising is deliberate. An unhandled exception surfaces as a bare ",[14,447,448],{},"#VALUE!"," with no explanation; a returned string lands in the cell and tells the user what went wrong.",[171,451,453],{"id":452},"step-3-take-ranges-as-dataframes","Step 3 — Take ranges as DataFrames",[25,455,35,461,35,464,35,467,35,470,35,476,35,481,35,487,35,490,35,494,35,499,35,502,35,504,35,507,35,509,35,513,35,515,35,518,35,521,35,524,35,526,35,529,35,533,35,536,35,542,35,548,35,551,35,555,35,558,35,564,35,569,35,573,35,577],{"viewBox":456,"role":28,"ariaLabel":457,"ariaLabelledBy":458,"xmlns":33,"style":34},"0 0 800 226","The arg decorator converting a worksheet range into a DataFrame: header row becomes column names, no column is treated as an index, and the values arrive typed.",[459,460],"argc-t","argc-d",[37,462,463],{"id":459},"How a worksheet range becomes a DataFrame",[41,465,466],{"id":460},"A four-row range on the sheet, whose first row holds the column headings region and revenue. The arg decorator with header set to True and index set to False turns that into a DataFrame with two named columns and three data rows. Getting the two options wrong is what makes the header appear as a data row, or makes the first column vanish into the index.",[45,468],{"x":47,"y":47,"width":48,"height":469,"fill":50},"226",[62,471,475],{"x":472,"y":473,"style":474},"150","30","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","the range A1:B4",[45,477],{"x":473,"y":104,"width":478,"height":479,"rx":480,"fill":132,"stroke":133,"style":60},"120","28","5",[62,482,486],{"x":483,"y":484,"style":485},"90","63","font-size:10.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","region",[45,488],{"x":489,"y":104,"width":478,"height":479,"rx":480,"fill":132,"stroke":133,"style":60},"154",[62,491,493],{"x":492,"y":484,"style":485},"214","revenue",[45,495],{"x":473,"y":496,"width":478,"height":497,"rx":480,"fill":50,"stroke":498},"76","26","var(--line,#cdd5e6)",[62,500,501],{"x":483,"y":56,"style":113},"North",[45,503],{"x":489,"y":496,"width":478,"height":497,"rx":480,"fill":50,"stroke":498},[62,505,506],{"x":492,"y":56,"style":113},"5150.00",[45,508],{"x":473,"y":64,"width":478,"height":497,"rx":480,"fill":50,"stroke":498},[62,510,512],{"x":483,"y":511,"style":113},"124","South",[45,514],{"x":489,"y":64,"width":478,"height":497,"rx":480,"fill":50,"stroke":498},[62,516,517],{"x":492,"y":511,"style":113},"4268.50",[45,519],{"x":473,"y":520,"width":478,"height":497,"rx":480,"fill":50,"stroke":498},"136",[62,522,523],{"x":483,"y":489,"style":113},"West",[45,525],{"x":489,"y":520,"width":478,"height":497,"rx":480,"fill":50,"stroke":498},[62,527,528],{"x":492,"y":489,"style":113},"3511.25",[83,530],{"x1":531,"y1":70,"x2":532,"y2":70,"stroke":133,"style":60},"286","322",[89,534],{"points":535,"fill":164},"330,102 318,96 318,108",[45,537],{"x":538,"y":54,"width":539,"height":540,"rx":541,"fill":99,"stroke":100,"style":60},"338","212","66","12",[62,543,547],{"x":544,"y":545,"style":546},"444","96","font-size:11px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","@xw.arg(\"data\", pd.DataFrame,",[62,549,550],{"x":544,"y":112,"style":546},"index=False, header=True)",[83,552],{"x1":553,"y1":70,"x2":554,"y2":70,"stroke":100,"style":60},"550","586",[89,556],{"points":557,"fill":127},"594,102 582,96 582,108",[45,559],{"x":560,"y":561,"width":562,"height":563,"rx":541,"fill":58,"stroke":59,"style":60},"602","62","182","82",[62,565,568],{"x":566,"y":483,"style":567},"693","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","DataFrame",[62,570,572],{"x":566,"y":571,"style":113},"112","2 named columns",[62,574,576],{"x":566,"y":575,"style":113},"130","3 data rows",[62,578,582],{"x":579,"y":580,"style":581},"400","192","font-size:11px;fill:var(--muted,#5b6780);text-anchor:middle","header=False makes the headings a data row; index=True makes \"region\" vanish into the index",[10,584,585,586,589],{},"The ",[14,587,588],{},"@xw.arg"," decorator converts an incoming range before your function sees it, which is what makes UDFs pleasant to write:",[176,591,593],{"className":292,"code":592,"language":294,"meta":181,"style":181},"import numpy as np\nimport pandas as pd\nimport xlwings as xw\n\n@xw.func\n@xw.arg(\"data\", pd.DataFrame, index=False, header=True)\ndef forecast_total(data, growth=0.04, periods=12):\n    \"\"\"Project a monthly revenue column forward and return the total.\"\"\"\n    if \"revenue\" not in data.columns:\n        return \"#VALUE! expected a 'revenue' column\"\n\n    revenue = pd.to_numeric(data[\"revenue\"], errors=\"coerce\").dropna()\n    if revenue.empty:\n        return \"#VALUE! no numeric revenue values\"\n\n    base = float(revenue.iloc[-1])\n    projected = base * ((1 + growth) ** np.arange(1, periods + 1))\n    return float(revenue.sum() + projected.sum())\n",[14,594,595,607,619,629,633,637,674,699,704,720,728,733,761,769,777,782,803,847],{"__ignoreMap":181},[185,596,597,599,602,604],{"class":83,"line":187},[185,598,308],{"class":307},[185,600,601],{"class":311}," numpy ",[185,603,315],{"class":307},[185,605,606],{"class":311}," np\n",[185,608,609,611,614,616],{"class":83,"line":201},[185,610,308],{"class":307},[185,612,613],{"class":311}," pandas ",[185,615,315],{"class":307},[185,617,618],{"class":311}," pd\n",[185,620,621,623,625,627],{"class":83,"line":321},[185,622,308],{"class":307},[185,624,312],{"class":311},[185,626,315],{"class":307},[185,628,318],{"class":311},[185,630,631],{"class":83,"line":328},[185,632,325],{"emptyLinePlaceholder":324},[185,634,635],{"class":83,"line":335},[185,636,332],{"class":331},[185,638,639,641,644,647,650,654,657,660,663,666,668,671],{"class":83,"line":347},[185,640,588],{"class":331},[185,642,643],{"class":311},"(",[185,645,646],{"class":194},"\"data\"",[185,648,649],{"class":311},", pd.DataFrame, ",[185,651,653],{"class":652},"sa561","index",[185,655,656],{"class":307},"=",[185,658,659],{"class":259},"False",[185,661,662],{"class":311},", ",[185,664,665],{"class":652},"header",[185,667,656],{"class":307},[185,669,670],{"class":259},"True",[185,672,673],{"class":311},")\n",[185,675,676,678,681,684,686,689,692,694,696],{"class":83,"line":353},[185,677,338],{"class":307},[185,679,680],{"class":331}," forecast_total",[185,682,683],{"class":311},"(data, growth",[185,685,656],{"class":307},[185,687,688],{"class":259},"0.04",[185,690,691],{"class":311},", periods",[185,693,656],{"class":307},[185,695,541],{"class":259},[185,697,698],{"class":311},"):\n",[185,700,701],{"class":83,"line":381},[185,702,703],{"class":194},"    \"\"\"Project a monthly revenue column forward and return the total.\"\"\"\n",[185,705,706,708,711,714,717],{"class":83,"line":390},[185,707,356],{"class":307},[185,709,710],{"class":194}," \"revenue\"",[185,712,713],{"class":307}," not",[185,715,716],{"class":307}," in",[185,718,719],{"class":311}," data.columns:\n",[185,721,723,725],{"class":83,"line":722},10,[185,724,384],{"class":307},[185,726,727],{"class":194}," \"#VALUE! expected a 'revenue' column\"\n",[185,729,731],{"class":83,"line":730},11,[185,732,325],{"emptyLinePlaceholder":324},[185,734,736,739,741,744,747,750,753,755,758],{"class":83,"line":735},12,[185,737,738],{"class":311},"    revenue ",[185,740,656],{"class":307},[185,742,743],{"class":311}," pd.to_numeric(data[",[185,745,746],{"class":194},"\"revenue\"",[185,748,749],{"class":311},"], ",[185,751,752],{"class":652},"errors",[185,754,656],{"class":307},[185,756,757],{"class":194},"\"coerce\"",[185,759,760],{"class":311},").dropna()\n",[185,762,764,766],{"class":83,"line":763},13,[185,765,356],{"class":307},[185,767,768],{"class":311}," revenue.empty:\n",[185,770,772,774],{"class":83,"line":771},14,[185,773,384],{"class":307},[185,775,776],{"class":194}," \"#VALUE! no numeric revenue values\"\n",[185,778,780],{"class":83,"line":779},15,[185,781,325],{"emptyLinePlaceholder":324},[185,783,785,788,790,793,796,798,800],{"class":83,"line":784},16,[185,786,787],{"class":311},"    base ",[185,789,656],{"class":307},[185,791,792],{"class":259}," float",[185,794,795],{"class":311},"(revenue.iloc[",[185,797,420],{"class":307},[185,799,411],{"class":259},[185,801,802],{"class":311},"])\n",[185,804,806,809,811,814,817,820,822,825,828,830,833,835,838,841,844],{"class":83,"line":805},17,[185,807,808],{"class":311},"    projected ",[185,810,656],{"class":307},[185,812,813],{"class":311}," base ",[185,815,816],{"class":307},"*",[185,818,819],{"class":311}," ((",[185,821,411],{"class":259},[185,823,824],{"class":307}," +",[185,826,827],{"class":311}," growth) ",[185,829,405],{"class":307},[185,831,832],{"class":311}," np.arange(",[185,834,411],{"class":259},[185,836,837],{"class":311},", periods ",[185,839,840],{"class":307},"+",[185,842,843],{"class":259}," 1",[185,845,846],{"class":311},"))\n",[185,848,850,852,854,857,859],{"class":83,"line":849},18,[185,851,393],{"class":307},[185,853,792],{"class":259},[185,855,856],{"class":311},"(revenue.sum() ",[185,858,840],{"class":307},[185,860,861],{"class":311}," projected.sum())\n",[176,863,866],{"className":864,"code":865,"language":62,"meta":181},[434],"=forecast_total(A1:B13, 0.04, 12)\n",[14,867,865],{"__ignoreMap":181},[10,869,585,870,873],{},[14,871,872],{},"index=False, header=True"," pair tells xlwings that the first row of the range is a header and no column should be treated as an index — which matches how a spreadsheet table is usually laid out. Getting these wrong is the usual reason a DataFrame arrives with the header as data.",[875,876,877,890],"table",{},[878,879,880],"thead",{},[881,882,883,887],"tr",{},[884,885,886],"th",{},"Converter",[884,888,889],{},"Use for",[891,892,893,904,914,924,934],"tbody",{},[881,894,895,901],{},[896,897,898],"td",{},[14,899,900],{},"pd.DataFrame",[896,902,903],{},"a tabular range with headers",[881,905,906,911],{},[896,907,908],{},[14,909,910],{},"pd.Series",[896,912,913],{},"a single labelled column",[881,915,916,921],{},[896,917,918],{},[14,919,920],{},"np.array",[896,922,923],{},"a numeric block, no labels",[881,925,926,931],{},[896,927,928],{},[14,929,930],{},"list",[896,932,933],{},"a simple one- or two-dimensional range",[881,935,936,941],{},[896,937,938],{},[14,939,940],{},"dict",[896,942,943],{},"a two-column key\u002Fvalue range",[171,945,947],{"id":946},"step-4-return-a-table","Step 4 — Return a table",[10,949,950],{},"Return a DataFrame and Excel spills it across a range — on current versions using dynamic arrays, so the user types one formula and gets a block:",[176,952,954],{"className":292,"code":953,"language":294,"meta":181,"style":181},"import pandas as pd\nimport xlwings as xw\n\n@xw.func\n@xw.arg(\"data\", pd.DataFrame, index=False, header=True)\n@xw.ret(index=False, header=True)\ndef summarise_by_region(data):\n    \"\"\"Return a region-level summary as a spilled table.\"\"\"\n    numeric = data.copy()\n    numeric[\"revenue\"] = pd.to_numeric(numeric[\"revenue\"], errors=\"coerce\")\n\n    return (\n        numeric.groupby(\"region\", as_index=False)\n               .agg(orders=(\"revenue\", \"size\"),\n                    revenue=(\"revenue\", \"sum\"),\n                    average=(\"revenue\", \"mean\"))\n               .round(2)\n               .sort_values(\"revenue\", ascending=False)\n    )\n",[14,955,956,966,976,980,984,1010,1033,1043,1048,1058,1085,1089,1096,1115,1137,1155,1173,1183,1201],{"__ignoreMap":181},[185,957,958,960,962,964],{"class":83,"line":187},[185,959,308],{"class":307},[185,961,613],{"class":311},[185,963,315],{"class":307},[185,965,618],{"class":311},[185,967,968,970,972,974],{"class":83,"line":201},[185,969,308],{"class":307},[185,971,312],{"class":311},[185,973,315],{"class":307},[185,975,318],{"class":311},[185,977,978],{"class":83,"line":321},[185,979,325],{"emptyLinePlaceholder":324},[185,981,982],{"class":83,"line":328},[185,983,332],{"class":331},[185,985,986,988,990,992,994,996,998,1000,1002,1004,1006,1008],{"class":83,"line":335},[185,987,588],{"class":331},[185,989,643],{"class":311},[185,991,646],{"class":194},[185,993,649],{"class":311},[185,995,653],{"class":652},[185,997,656],{"class":307},[185,999,659],{"class":259},[185,1001,662],{"class":311},[185,1003,665],{"class":652},[185,1005,656],{"class":307},[185,1007,670],{"class":259},[185,1009,673],{"class":311},[185,1011,1012,1015,1017,1019,1021,1023,1025,1027,1029,1031],{"class":83,"line":347},[185,1013,1014],{"class":331},"@xw.ret",[185,1016,643],{"class":311},[185,1018,653],{"class":652},[185,1020,656],{"class":307},[185,1022,659],{"class":259},[185,1024,662],{"class":311},[185,1026,665],{"class":652},[185,1028,656],{"class":307},[185,1030,670],{"class":259},[185,1032,673],{"class":311},[185,1034,1035,1037,1040],{"class":83,"line":353},[185,1036,338],{"class":307},[185,1038,1039],{"class":331}," summarise_by_region",[185,1041,1042],{"class":311},"(data):\n",[185,1044,1045],{"class":83,"line":381},[185,1046,1047],{"class":194},"    \"\"\"Return a region-level summary as a spilled table.\"\"\"\n",[185,1049,1050,1053,1055],{"class":83,"line":390},[185,1051,1052],{"class":311},"    numeric ",[185,1054,656],{"class":307},[185,1056,1057],{"class":311}," data.copy()\n",[185,1059,1060,1063,1065,1068,1070,1073,1075,1077,1079,1081,1083],{"class":83,"line":722},[185,1061,1062],{"class":311},"    numeric[",[185,1064,746],{"class":194},[185,1066,1067],{"class":311},"] ",[185,1069,656],{"class":307},[185,1071,1072],{"class":311}," pd.to_numeric(numeric[",[185,1074,746],{"class":194},[185,1076,749],{"class":311},[185,1078,752],{"class":652},[185,1080,656],{"class":307},[185,1082,757],{"class":194},[185,1084,673],{"class":311},[185,1086,1087],{"class":83,"line":730},[185,1088,325],{"emptyLinePlaceholder":324},[185,1090,1091,1093],{"class":83,"line":735},[185,1092,393],{"class":307},[185,1094,1095],{"class":311}," (\n",[185,1097,1098,1101,1104,1106,1109,1111,1113],{"class":83,"line":763},[185,1099,1100],{"class":311},"        numeric.groupby(",[185,1102,1103],{"class":194},"\"region\"",[185,1105,662],{"class":311},[185,1107,1108],{"class":652},"as_index",[185,1110,656],{"class":307},[185,1112,659],{"class":259},[185,1114,673],{"class":311},[185,1116,1117,1120,1123,1125,1127,1129,1131,1134],{"class":83,"line":771},[185,1118,1119],{"class":311},"               .agg(",[185,1121,1122],{"class":652},"orders",[185,1124,656],{"class":307},[185,1126,643],{"class":311},[185,1128,746],{"class":194},[185,1130,662],{"class":311},[185,1132,1133],{"class":194},"\"size\"",[185,1135,1136],{"class":311},"),\n",[185,1138,1139,1142,1144,1146,1148,1150,1153],{"class":83,"line":779},[185,1140,1141],{"class":652},"                    revenue",[185,1143,656],{"class":307},[185,1145,643],{"class":311},[185,1147,746],{"class":194},[185,1149,662],{"class":311},[185,1151,1152],{"class":194},"\"sum\"",[185,1154,1136],{"class":311},[185,1156,1157,1160,1162,1164,1166,1168,1171],{"class":83,"line":784},[185,1158,1159],{"class":652},"                    average",[185,1161,656],{"class":307},[185,1163,643],{"class":311},[185,1165,746],{"class":194},[185,1167,662],{"class":311},[185,1169,1170],{"class":194},"\"mean\"",[185,1172,846],{"class":311},[185,1174,1175,1178,1181],{"class":83,"line":805},[185,1176,1177],{"class":311},"               .round(",[185,1179,1180],{"class":259},"2",[185,1182,673],{"class":311},[185,1184,1185,1188,1190,1192,1195,1197,1199],{"class":83,"line":849},[185,1186,1187],{"class":311},"               .sort_values(",[185,1189,746],{"class":194},[185,1191,662],{"class":311},[185,1193,1194],{"class":652},"ascending",[185,1196,656],{"class":307},[185,1198,659],{"class":259},[185,1200,673],{"class":311},[185,1202,1204],{"class":83,"line":1203},19,[185,1205,1206],{"class":311},"    )\n",[10,1208,1209,1211,1212,1214,1215,1218],{},[14,1210,1014],{}," controls the return conversion the way ",[14,1213,588],{}," controls the input. Setting ",[14,1216,1217],{},"header=True"," writes the column names into the first spilled row, which is almost always what a reader wants.",[171,1220,1222],{"id":1221},"step-5-design-for-the-round-trip","Step 5 — Design for the round trip",[10,1224,1225],{},"Every call crosses a process boundary. That cost is small in absolute terms and enormous when multiplied by a filled-down column.",[25,1227,35,1233,35,1236,35,1239,35,1242,35,1248,35,1253,35,1258,35,1317,35,1321,35,1325,35,1328,35,1331,35,1335,35,1338,35,1342,35,1345,35,1348],{"viewBox":1228,"role":28,"ariaLabel":1229,"ariaLabelledBy":1230,"xmlns":33,"style":34},"0 0 800 234","Per-row calls versus one range call: filling a formula down a thousand rows makes a thousand process crossings, while one call over the whole range crosses once.",[1231,1232],"rt-t","rt-d",[37,1234,1235],{"id":1231},"Design the function so one call does all the work",[41,1237,1238],{"id":1232},"Two designs. A scalar function filled down a thousand rows makes a thousand separate crossings between Excel and Python, each carrying a fixed overhead that dwarfs the calculation. A range function called once takes the whole block, computes everything in vectorised pandas, and spills a whole block back — one crossing regardless of row count.",[45,1240],{"x":47,"y":47,"width":48,"height":1241,"fill":50},"234",[45,1243],{"x":53,"y":1244,"width":1245,"height":580,"rx":53,"fill":1246,"stroke":1247,"style":60},"24","368","#fee8f2","var(--accent,#f43f8f)",[62,1249,1252],{"x":85,"y":1250,"style":1251},"52","font-size:12.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","scalar UDF, filled down",[62,1254,1257],{"x":85,"y":1255,"style":1256},"80","font-size:11px;fill:var(--text,#172033);text-anchor:middle","=RATE(B2, C2) in 1,000 cells",[1259,1260,1262,1263,1262,1268,1262,1271,1262,1274,1262,1277,1262,1280,1262,1283,1262,1286,1262,1289,1262,1292,1262,1295,1262,1297,1262,1300,1262,1302,1262,1305,1262,1308,1262,1311,1262,1314,35],"g",{"fill":1261},"#f43f8f","\n    ",[45,1264],{"x":1250,"y":1265,"width":1266,"height":1267,"rx":1180},"98","10","22",[45,1269],{"x":1270,"y":1265,"width":1266,"height":1267,"rx":1180},"68",[45,1272],{"x":1273,"y":1265,"width":1266,"height":1267,"rx":1180},"84",[45,1275],{"x":1276,"y":1265,"width":1266,"height":1267,"rx":1180},"100",[45,1278],{"x":1279,"y":1265,"width":1266,"height":1267,"rx":1180},"116",[45,1281],{"x":1282,"y":1265,"width":1266,"height":1267,"rx":1180},"132",[45,1284],{"x":1285,"y":1265,"width":1266,"height":1267,"rx":1180},"148",[45,1287],{"x":1288,"y":1265,"width":1266,"height":1267,"rx":1180},"164",[45,1290],{"x":1291,"y":1265,"width":1266,"height":1267,"rx":1180},"180",[45,1293],{"x":1294,"y":1265,"width":1266,"height":1267,"rx":1180},"196",[45,1296],{"x":539,"y":1265,"width":1266,"height":1267,"rx":1180},[45,1298],{"x":1299,"y":1265,"width":1266,"height":1267,"rx":1180},"228",[45,1301],{"x":49,"y":1265,"width":1266,"height":1267,"rx":1180},[45,1303],{"x":1304,"y":1265,"width":1266,"height":1267,"rx":1180},"260",[45,1306],{"x":1307,"y":1265,"width":1266,"height":1267,"rx":1180},"276",[45,1309],{"x":1310,"y":1265,"width":1266,"height":1267,"rx":1180},"292",[45,1312],{"x":1313,"y":1265,"width":1266,"height":1267,"rx":1180},"308",[45,1315],{"x":1316,"y":1265,"width":1266,"height":1267,"rx":1180},"324",[62,1318,1320],{"x":85,"y":79,"style":1319},"font-size:11px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","1,000 process crossings",[62,1322,1324],{"x":85,"y":1323,"style":154},"170","recalculated on every sheet change",[62,1326,1327],{"x":85,"y":1294,"style":154},"the workbook becomes unusable",[45,1329],{"x":1330,"y":1244,"width":1245,"height":580,"rx":53,"fill":58,"stroke":59,"style":60},"418",[62,1332,1334],{"x":560,"y":1250,"style":1333},"font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","range UDF, called once",[62,1336,1337],{"x":560,"y":1255,"style":1256},"=SUMMARISE(A1:C1001)",[45,1339],{"x":1340,"y":1265,"width":1250,"height":1267,"rx":1341,"fill":92},"452","3",[62,1343,1344],{"x":560,"y":79,"style":71},"one crossing",[62,1346,1347],{"x":560,"y":1323,"style":154},"vectorised pandas inside",[62,1349,1350],{"x":560,"y":1294,"style":154},"spills the whole result back",[10,1352,1353,1354,1357],{},"Two decorators help when a scalar function is genuinely the right shape. ",[14,1355,1356],{},"@xw.func(volatile=False)"," — the default — tells Excel the result depends only on the arguments, so it is not recalculated on every change elsewhere. And caching pays for itself when a function is called repeatedly with the same inputs:",[176,1359,1361],{"className":292,"code":1360,"language":294,"meta":181,"style":181},"from functools import lru_cache\nimport xlwings as xw\n\n@lru_cache(maxsize=512)\ndef _lookup(code):\n    \"\"\"Expensive work — a database or API call — cached across calls.\"\"\"\n    return fetch_rate_from_source(code)\n\n@xw.func\ndef rate_for(code):\n    try:\n        return _lookup(str(code).strip().upper())\n    except Exception as exc:\n        return f\"#VALUE! {exc}\"\n",[14,1362,1363,1376,1386,1390,1407,1417,1422,1429,1433,1437,1446,1453,1466,1480],{"__ignoreMap":181},[185,1364,1365,1368,1371,1373],{"class":83,"line":187},[185,1366,1367],{"class":307},"from",[185,1369,1370],{"class":311}," functools ",[185,1372,308],{"class":307},[185,1374,1375],{"class":311}," lru_cache\n",[185,1377,1378,1380,1382,1384],{"class":83,"line":201},[185,1379,308],{"class":307},[185,1381,312],{"class":311},[185,1383,315],{"class":307},[185,1385,318],{"class":311},[185,1387,1388],{"class":83,"line":321},[185,1389,325],{"emptyLinePlaceholder":324},[185,1391,1392,1395,1397,1400,1402,1405],{"class":83,"line":328},[185,1393,1394],{"class":331},"@lru_cache",[185,1396,643],{"class":311},[185,1398,1399],{"class":652},"maxsize",[185,1401,656],{"class":307},[185,1403,1404],{"class":259},"512",[185,1406,673],{"class":311},[185,1408,1409,1411,1414],{"class":83,"line":335},[185,1410,338],{"class":307},[185,1412,1413],{"class":331}," _lookup",[185,1415,1416],{"class":311},"(code):\n",[185,1418,1419],{"class":83,"line":347},[185,1420,1421],{"class":194},"    \"\"\"Expensive work — a database or API call — cached across calls.\"\"\"\n",[185,1423,1424,1426],{"class":83,"line":353},[185,1425,393],{"class":307},[185,1427,1428],{"class":311}," fetch_rate_from_source(code)\n",[185,1430,1431],{"class":83,"line":381},[185,1432,325],{"emptyLinePlaceholder":324},[185,1434,1435],{"class":83,"line":390},[185,1436,332],{"class":331},[185,1438,1439,1441,1444],{"class":83,"line":722},[185,1440,338],{"class":307},[185,1442,1443],{"class":331}," rate_for",[185,1445,1416],{"class":311},[185,1447,1448,1451],{"class":83,"line":730},[185,1449,1450],{"class":307},"    try",[185,1452,378],{"class":311},[185,1454,1455,1457,1460,1463],{"class":83,"line":735},[185,1456,384],{"class":307},[185,1458,1459],{"class":311}," _lookup(",[185,1461,1462],{"class":259},"str",[185,1464,1465],{"class":311},"(code).strip().upper())\n",[185,1467,1468,1471,1474,1477],{"class":83,"line":763},[185,1469,1470],{"class":307},"    except",[185,1472,1473],{"class":259}," Exception",[185,1475,1476],{"class":307}," as",[185,1478,1479],{"class":311}," exc:\n",[185,1481,1482,1484,1487,1490,1494,1497,1500],{"class":83,"line":771},[185,1483,384],{"class":307},[185,1485,1486],{"class":307}," f",[185,1488,1489],{"class":194},"\"#VALUE! ",[185,1491,1493],{"class":1492},"sSjpA","{",[185,1495,1496],{"class":311},"exc",[185,1498,1499],{"class":1492},"}",[185,1501,1502],{"class":194},"\"\n",[10,1504,1505,1506,1509],{},"Note that ",[14,1507,1508],{},"lru_cache"," lives on the private helper, not the decorated function — xlwings needs the real function object to register it, and caching the wrapper can confuse the argument conversion.",[171,1511,1513],{"id":1512},"common-pitfalls-and-fixes","Common pitfalls and fixes",[875,1515,1516,1529],{},[878,1517,1518],{},[881,1519,1520,1523,1526],{},[884,1521,1522],{},"Symptom",[884,1524,1525],{},"Cause",[884,1527,1528],{},"Fix",[891,1530,1531,1544,1557,1568,1581,1594,1609,1620],{},[881,1532,1533,1538,1541],{},[896,1534,1535,1537],{},[14,1536,232],{}," on every call",[896,1539,1540],{},"Add-in missing, or functions not imported",[896,1542,1543],{},"Install the add-in; click Import Functions.",[881,1545,1546,1551,1554],{},[896,1547,1548,1550],{},[14,1549,232],{}," after renaming",[896,1552,1553],{},"Module name no longer matches the workbook",[896,1555,1556],{},"Keep the stems identical.",[881,1558,1559,1562,1565],{},[896,1560,1561],{},"Add-in cannot register functions",[896,1563,1564],{},"VBA object model not trusted",[896,1566,1567],{},"Enable it in Trust Center → Macro Settings.",[881,1569,1570,1573,1576],{},[896,1571,1572],{},"Header row treated as data",[896,1574,1575],{},"Converter options wrong",[896,1577,1578,23],{},[14,1579,1580],{},"@xw.arg(..., header=True, index=False)",[881,1582,1583,1588,1591],{},[896,1584,1585,1587],{},[14,1586,448],{}," with no detail",[896,1589,1590],{},"Exception raised inside the function",[896,1592,1593],{},"Catch it and return the message as a string.",[881,1595,1596,1599,1602],{},[896,1597,1598],{},"Workbook recalculates constantly",[896,1600,1601],{},"Function marked volatile",[896,1603,1604,1605,1608],{},"Leave ",[14,1606,1607],{},"volatile=False"," (the default).",[881,1610,1611,1614,1617],{},[896,1612,1613],{},"Very slow with a filled-down formula",[896,1615,1616],{},"One crossing per cell",[896,1618,1619],{},"Redesign to take and return a range.",[881,1621,1622,1625,1628],{},[896,1623,1624],{},"Nothing works on macOS",[896,1626,1627],{},"UDFs are Windows-only",[896,1629,1630],{},"Use xlwings scripts, or generate the file.",[171,1632,1634],{"id":1633},"performance-and-scale-notes","Performance and scale notes",[10,1636,1637],{},"The dominant cost is the boundary crossing, not your Python. A trivial scalar function still pays the full COM round trip, so the practical guidance is entirely about call count.",[10,1639,1640,1643],{},[214,1641,1642],{},"Take a range, return a range."," One call over a thousand rows is roughly a thousand times cheaper than a thousand calls over one row each, and the pandas work inside is vectorised anyway.",[10,1645,1646,1649],{},[214,1647,1648],{},"Keep imports at module level."," Importing pandas inside the function body re-runs the lookup on every call; at module level it happens once when the interpreter starts.",[10,1651,1652,1655,1656,1658,1659,1663],{},[214,1653,1654],{},"Cache anything external."," A UDF that queries a database on each call will make Excel feel broken. ",[14,1657,1508],{}," on the helper turns a thousand identical lookups into one — and where the source is a real database, consider pulling the whole table once with the approach in ",[19,1660,1662],{"href":1661},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Frefresh-an-excel-report-from-a-database-on-a-schedule\u002F","refreshing an Excel report from a database on a schedule"," rather than querying per cell.",[10,1665,1666],{},"There is also a design question worth asking before building UDFs at all. They tie the workbook to a Windows machine with Python and the add-in installed — which is fine for a small analyst team and a real obstacle for anything distributed widely. Where the calculation can happen ahead of time, generating a finished workbook is more portable and needs nothing installed on the reader's machine:",[176,1668,1670],{"className":292,"code":1669,"language":294,"meta":181,"style":181},"# Often the better answer: compute in Python, ship a plain .xlsx.\nsummary = summarise_by_region(data)\nsummary.to_excel(\"regional_summary.xlsx\", index=False, engine=\"xlsxwriter\")\n",[14,1671,1672,1677,1687],{"__ignoreMap":181},[185,1673,1674],{"class":83,"line":187},[185,1675,1676],{"class":301},"# Often the better answer: compute in Python, ship a plain .xlsx.\n",[185,1678,1679,1682,1684],{"class":83,"line":201},[185,1680,1681],{"class":311},"summary ",[185,1683,656],{"class":307},[185,1685,1686],{"class":311}," summarise_by_region(data)\n",[185,1688,1689,1692,1695,1697,1699,1701,1703,1705,1708,1710,1713],{"class":83,"line":321},[185,1690,1691],{"class":311},"summary.to_excel(",[185,1693,1694],{"class":194},"\"regional_summary.xlsx\"",[185,1696,662],{"class":311},[185,1698,653],{"class":652},[185,1700,656],{"class":307},[185,1702,659],{"class":259},[185,1704,662],{"class":311},[185,1706,1707],{"class":652},"engine",[185,1709,656],{"class":307},[185,1711,1712],{"class":194},"\"xlsxwriter\"",[185,1714,673],{"class":311},[10,1716,1717],{},"Reserve UDFs for the case that genuinely needs them: a user typing new inputs and wanting your logic to respond interactively.",[171,1719,1721],{"id":1720},"conclusion","Conclusion",[10,1723,1724,1725,221,1727,1729,1730,1732],{},"An xlwings UDF turns a Python function into a worksheet formula with two decorators and an add-in click. Match the module name to the workbook, trust the VBA object model, and use ",[14,1726,588],{},[14,1728,900],{}," so ranges arrive in a shape worth working with. Return error strings rather than raising, so users see a reason instead of a bare ",[14,1731,448],{},". Above all, design for the round trip: take a whole range and spill a whole range back, because a scalar function filled down a thousand rows makes a thousand crossings and turns a workbook unusable. And remember it is Windows-only — if the report just needs producing, generate the file instead.",[171,1734,1736],{"id":1735},"frequently-asked-questions","Frequently asked questions",[10,1738,1739,1742],{},[214,1740,1741],{},"Do xlwings UDFs work on macOS or Linux?","\nNo. User-defined functions require the COM automation layer that only exists in Excel for Windows. On macOS you can still drive Excel with xlwings scripts, and on Linux neither works — use openpyxl or xlsxwriter to produce files instead.",[10,1744,1745,1748,1749,1751],{},[214,1746,1747],{},"Why does Excel say my function name is not recognised?","\nEither the add-in is not installed, the workbook's module name does not match the Python file, or you have not clicked Import Functions after adding or renaming a function. All three produce the same ",[14,1750,232],{}," error.",[10,1753,1754,1757],{},[214,1755,1756],{},"Can a UDF return a whole table?","\nYes. Return a DataFrame or a list of lists and Excel spills it across a range. On current versions this uses dynamic arrays automatically; on older ones the caller must enter it as an array formula.",[10,1759,1760,1763],{},[214,1761,1762],{},"Are UDFs fast enough for a large model?","\nEach call crosses the process boundary between Excel and Python, so thousands of individual calls are slow. Design the function to take a whole range and return a whole range, so one call does the work of a thousand.",[10,1765,1766,1769,1770,1773],{},[214,1767,1768],{},"How do I debug a UDF that returns an error?","\nSet the add-in to debug mode and run the Python file directly, which attaches your interpreter to Excel so breakpoints work. Failing that, wrap the body in a ",[14,1771,1772],{},"try"," block and return the exception text so the message lands in the cell.",[171,1775,1777],{"id":1776},"related","Related",[1779,1780,1781,1788,1795,1802,1808],"ul",{},[1782,1783,1784,1785,1787],"li",{},"Up to the parent: ",[19,1786,22],{"href":21}," — the wider xlwings model.",[1782,1789,1790,1794],{},[19,1791,1793],{"href":1792},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fread-and-write-a-live-excel-workbook-with-xlwings\u002F","Read and Write a Live Excel Workbook with xlwings"," — driving Excel from Python, the other direction.",[1782,1796,1797,1801],{},[19,1798,1800],{"href":1799},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fxlwings-run-macro-from-python-example\u002F","xlwings: Run a Macro from Python"," — calling existing VBA rather than replacing it.",[1782,1803,1804,1807],{},[19,1805,1806],{"href":224},"openpyxl vs xlsxwriter vs pandas.ExcelWriter"," — the file-generating alternative to a UDF.",[1782,1809,1810,1813],{},[19,1811,1812],{"href":1661},"Refresh an Excel Report from a Database on a Schedule"," — pulling data once instead of per cell.",[1815,1816,1817],"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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}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}",{"title":181,"searchDepth":201,"depth":201,"links":1819},[1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830],{"id":173,"depth":201,"text":174},{"id":235,"depth":201,"text":236},{"id":282,"depth":201,"text":283},{"id":452,"depth":201,"text":453},{"id":946,"depth":201,"text":947},{"id":1221,"depth":201,"text":1222},{"id":1512,"depth":201,"text":1513},{"id":1633,"depth":201,"text":1634},{"id":1720,"depth":201,"text":1721},{"id":1735,"depth":201,"text":1736},{"id":1776,"depth":201,"text":1777},"2026-08-15","Expose a Python function as a worksheet formula with xlwings — the add-in setup, @xw.func and @xw.arg decorators, array and DataFrame arguments, and the Windows-only limits.","md",[1835,1837,1839,1841,1843],{"q":1741,"a":1836},"No. User-defined functions require the COM automation layer that only exists in Excel for Windows. On macOS you can still drive Excel with xlwings scripts, and on Linux neither works — use openpyxl or xlsxwriter to produce files instead.",{"q":1747,"a":1838},"Either the add-in is not installed, the workbook's module name does not match the Python file, or you have not clicked Import Functions after adding or renaming a function. All three produce the same",{"q":1756,"a":1840},"Yes. Return a DataFrame or a list of lists and Excel spills it across a range. On current versions this uses dynamic arrays automatically; on older ones the caller must enter it as an array formula.",{"q":1762,"a":1842},"Each call crosses the process boundary between Excel and Python, so thousands of individual calls are slow. Design the function to take a whole range and return a whole range, so one call does the work of a thousand.",{"q":1768,"a":1844},"Set the add-in to debug mode and run the Python file directly, which attaches your interpreter to Excel so breakpoints work. Failing that, wrap the body in a try block and return the exception text so the message lands in the cell.",{},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fcall-python-from-excel-with-xlwings-udf",{"title":1848,"description":1849},"xlwings UDF: Call Python from an Excel Formula","Build user-defined functions in Python that Excel calls like SUM — xlwings add-in install, @xw.func, DataFrame converters, dynamic arrays, caching and debugging.","call-python-from-excel-with-xlwings-udf","getting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fcall-python-from-excel-with-xlwings-udf\u002Findex","how-to","76tjt-VaKUYvkqbun2R1Q3ojQQuoMXqMXaOaQxKvUio",[1855,1859],{"title":1856,"path":1857,"stem":1858,"children":-1},"Automating Excel With xlwings: The Basics","\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics","getting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Findex",{"title":1793,"path":1860,"stem":1861,"children":-1},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fread-and-write-a-live-excel-workbook-with-xlwings","getting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fread-and-write-a-live-excel-workbook-with-xlwings\u002Findex",1786800028669]