[{"data":1,"prerenderedAt":1578},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fwhen-to-use-xlwings-instead-of-openpyxl":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fwhen-to-use-xlwings-instead-of-openpyxl":1569},{"id":4,"title":5,"body":6,"dateModified":1541,"datePublished":1541,"description":1542,"extension":1543,"faq":1544,"meta":1553,"navigation":239,"path":1562,"seo":1563,"slug":1565,"stem":1566,"type":1567,"__hash__":1568},"docs\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fwhen-to-use-xlwings-instead-of-openpyxl\u002Findex.md","When to Use xlwings Instead of openpyxl",{"type":7,"value":8,"toc":1526},"minimark",[9,19,149,154,179,187,191,211,340,348,352,355,477,484,488,500,603,610,614,621,689,696,700,703,803,809,816,820,823,938,948,952,1053,1057,1060,1063,1239,1254,1258,1339,1342,1439,1446,1450,1453,1457,1464,1470,1476,1482,1486,1522],[10,11,12,13,18],"p",{},"openpyxl edits the file; xlwings drives the application. That one sentence decides almost every case,\nbecause it identifies what you are actually paying for: xlwings needs Excel installed, runs only on\nWindows and macOS, and makes a cross-process call for every operation — and in exchange it gives you\nExcel's own calculation engine, its printing, its macros and its live connections. This guide, part\nof ",[14,15,17],"a",{"href":16},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002F","Choosing a Python Excel Library",",\nsets out the four jobs where that trade is worth making.",[20,21,29,30,29,34,29,38,29,45,29,55,29,62,29,69,29,74,29,78,29,82,29,86,29,91,29,96,29,100,29,103,29,106,29,109,29,112,29,121,29,127,29,132,29,137,29,141,29,144],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 760 224","img",[25,26],"xw-trade-t","xw-trade-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[31,32,33],"title",{"id":25},"What driving the application buys, and what it costs",[35,36,37],"desc",{"id":26},"openpyxl edits the file directly and runs anywhere, but cannot calculate formulas or keep every Excel object. xlwings drives a licensed copy of Excel, gaining recalculation and native output at the cost of platform and speed.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","760","224","#ffffff",[39,46],{"x":47,"y":48,"width":49,"height":50,"rx":51,"fill":52,"stroke":53,"style":54},"20","28","270.0","162","14","#d9f4f1","var(--teal,#0f9488)","stroke-width:2px",[56,57,61],"text",{"x":58,"y":59,"style":60},"155.0","54","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","openpyxl: the file",[63,64],"line",{"x1":65,"y1":66,"x2":67,"y2":66,"stroke":53,"style":68},"36","64","274.0","stroke-width:1px",[56,70,73],{"x":58,"y":71,"style":72},"86","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","runs on any server",[56,75,77],{"x":58,"y":76,"style":72},"109","fast, testable",[56,79,81],{"x":58,"y":80,"style":72},"132","no formula engine",[56,83,85],{"x":58,"y":84,"style":72},"155","drops some objects",[39,87],{"x":88,"y":48,"width":49,"height":50,"rx":51,"fill":89,"stroke":90,"style":54},"470.0","#fdefd8","var(--gold,#b4740a)",[56,92,95],{"x":93,"y":59,"style":94},"605.0","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","xlwings: the app",[63,97],{"x1":98,"y1":66,"x2":99,"y2":66,"stroke":90,"style":68},"486.0","724.0",[56,101,102],{"x":93,"y":71,"style":72},"Excel recalculates",[56,104,105],{"x":93,"y":76,"style":72},"keeps everything",[56,107,108],{"x":93,"y":80,"style":72},"Windows or macOS",[56,110,111],{"x":93,"y":84,"style":72},"a call per operation",[39,113],{"x":114,"y":115,"width":116,"height":117,"rx":118,"fill":119,"stroke":120},"316.0","90.0","128","38","19","#ebebfd","var(--brand,#5b5cf0)",[56,122,126],{"x":123,"y":124,"style":125},"380.0","114.0","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","trade-off",[63,128],{"x1":129,"y1":130,"x2":131,"y2":130,"stroke":120,"style":54},"295.0","109.0","309.0",[133,134],"polygon",{"points":135,"fill":136},"309.0,109.0 300.0,104.0 300.0,114.0","#5b5cf0",[63,138],{"x1":139,"y1":130,"x2":140,"y2":130,"stroke":120,"style":54},"449.0","463.0",[133,142],{"points":143,"fill":136},"463.0,109.0 454.0,104.0 454.0,114.0",[56,145,148],{"x":123,"y":146,"style":147},"210","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","pay for Excel only when the value comes from Excel",[150,151,153],"h2",{"id":152},"prerequisites","Prerequisites",[155,156,161],"pre",{"className":157,"code":158,"language":159,"meta":160,"style":160},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install xlwings\n","bash","",[162,163,164],"code",{"__ignoreMap":160},[165,166,168,172,176],"span",{"class":63,"line":167},1,[165,169,171],{"class":170},"sMTad","pip",[165,173,175],{"class":174},"srMev"," install",[165,177,178],{"class":174}," xlwings\n",[10,180,181,182,186],{},"Plus a licensed installation of Excel on Windows or macOS. On Windows, xlwings sits on top of\npywin32 and the same COM interface described in\n",[14,183,185],{"href":184},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002F","Automating Excel with COM and pywin32",".",[150,188,190],{"id":189},"job-one-you-need-recalculated-values","Job one: you need recalculated values",[10,192,193,194,197,198,201,202,205,206,210],{},"openpyxl does not evaluate formulas. It can write ",[162,195,196],{},"=SUM(B2:B99)"," into a cell, and it can read the\nvalue Excel cached there last time the file was open — but if no copy of Excel has opened the file\nsince, ",[162,199,200],{},"data_only=True"," returns ",[162,203,204],{},"None",". When the deliverable is the ",[207,208,209],"em",{},"result",", only something with a\ncalculation engine can produce it.",[155,212,216],{"className":213,"code":214,"language":215,"meta":160,"style":160},"language-python shiki shiki-themes github-light github-dark-high-contrast","import xlwings as xw\n\nwith xw.App(visible=False) as app:\n    book = app.books.open(\"model.xlsx\")\n    book.app.calculate()                      # Excel's own engine\n    total = book.sheets[\"Summary\"].range(\"C12\").value\n    book.save()\n    book.close()\nprint(total)\n","python",[162,217,218,234,241,269,286,296,319,325,331],{"__ignoreMap":160},[165,219,220,224,228,231],{"class":63,"line":167},[165,221,223],{"class":222},"s-kum","import",[165,225,227],{"class":226},"skGVy"," xlwings ",[165,229,230],{"class":222},"as",[165,232,233],{"class":226}," xw\n",[165,235,237],{"class":63,"line":236},2,[165,238,240],{"emptyLinePlaceholder":239},true,"\n",[165,242,244,247,250,254,257,261,264,266],{"class":63,"line":243},3,[165,245,246],{"class":222},"with",[165,248,249],{"class":226}," xw.App(",[165,251,253],{"class":252},"sa561","visible",[165,255,256],{"class":222},"=",[165,258,260],{"class":259},"sP0c6","False",[165,262,263],{"class":226},") ",[165,265,230],{"class":222},[165,267,268],{"class":226}," app:\n",[165,270,272,275,277,280,283],{"class":63,"line":271},4,[165,273,274],{"class":226},"    book ",[165,276,256],{"class":222},[165,278,279],{"class":226}," app.books.open(",[165,281,282],{"class":174},"\"model.xlsx\"",[165,284,285],{"class":226},")\n",[165,287,289,292],{"class":63,"line":288},5,[165,290,291],{"class":226},"    book.app.calculate()                      ",[165,293,295],{"class":294},"s-wDw","# Excel's own engine\n",[165,297,299,302,304,307,310,313,316],{"class":63,"line":298},6,[165,300,301],{"class":226},"    total ",[165,303,256],{"class":222},[165,305,306],{"class":226}," book.sheets[",[165,308,309],{"class":174},"\"Summary\"",[165,311,312],{"class":226},"].range(",[165,314,315],{"class":174},"\"C12\"",[165,317,318],{"class":226},").value\n",[165,320,322],{"class":63,"line":321},7,[165,323,324],{"class":226},"    book.save()\n",[165,326,328],{"class":63,"line":327},8,[165,329,330],{"class":226},"    book.close()\n",[165,332,334,337],{"class":63,"line":333},9,[165,335,336],{"class":259},"print",[165,338,339],{"class":226},"(total)\n",[10,341,342,343,347],{},"The pure-Python alternative is to compute the number yourself in pandas and write it as a literal,\nwhich is faster and testable — and the right answer whenever the formula is yours. xlwings earns its\nplace when the formulas belong to a model somebody else maintains.\n",[14,344,346],{"href":345},"\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","\ncovers the middle ground.",[150,349,351],{"id":350},"job-two-the-workbook-contains-things-openpyxl-drops","Job two: the workbook contains things openpyxl drops",[10,353,354],{},"Pivot table caches, slicers, some chart types, ActiveX controls and Power Query connections do not\nsurvive an openpyxl round trip. If the file must keep them and must also be edited, the edit has to\nhappen through Excel.",[155,356,358],{"className":213,"code":357,"language":215,"meta":160,"style":160},"import xlwings as xw\n\nwith xw.App(visible=False) as app:\n    book = app.books.open(\"dashboard.xlsm\")\n    book.sheets[\"Data\"].range(\"A1\").options(index=False).value = new_frame\n    for sheet in book.sheets:\n        for table in sheet.api.PivotTables():\n            table.RefreshTable()\n    book.save()\n    book.close()\n",[162,359,360,370,374,392,405,436,450,463,468,472],{"__ignoreMap":160},[165,361,362,364,366,368],{"class":63,"line":167},[165,363,223],{"class":222},[165,365,227],{"class":226},[165,367,230],{"class":222},[165,369,233],{"class":226},[165,371,372],{"class":63,"line":236},[165,373,240],{"emptyLinePlaceholder":239},[165,375,376,378,380,382,384,386,388,390],{"class":63,"line":243},[165,377,246],{"class":222},[165,379,249],{"class":226},[165,381,253],{"class":252},[165,383,256],{"class":222},[165,385,260],{"class":259},[165,387,263],{"class":226},[165,389,230],{"class":222},[165,391,268],{"class":226},[165,393,394,396,398,400,403],{"class":63,"line":271},[165,395,274],{"class":226},[165,397,256],{"class":222},[165,399,279],{"class":226},[165,401,402],{"class":174},"\"dashboard.xlsm\"",[165,404,285],{"class":226},[165,406,407,410,413,415,418,421,424,426,428,431,433],{"class":63,"line":288},[165,408,409],{"class":226},"    book.sheets[",[165,411,412],{"class":174},"\"Data\"",[165,414,312],{"class":226},[165,416,417],{"class":174},"\"A1\"",[165,419,420],{"class":226},").options(",[165,422,423],{"class":252},"index",[165,425,256],{"class":222},[165,427,260],{"class":259},[165,429,430],{"class":226},").value ",[165,432,256],{"class":222},[165,434,435],{"class":226}," new_frame\n",[165,437,438,441,444,447],{"class":63,"line":298},[165,439,440],{"class":222},"    for",[165,442,443],{"class":226}," sheet ",[165,445,446],{"class":222},"in",[165,448,449],{"class":226}," book.sheets:\n",[165,451,452,455,458,460],{"class":63,"line":321},[165,453,454],{"class":222},"        for",[165,456,457],{"class":226}," table ",[165,459,446],{"class":222},[165,461,462],{"class":226}," sheet.api.PivotTables():\n",[165,464,465],{"class":63,"line":327},[165,466,467],{"class":226},"            table.RefreshTable()\n",[165,469,470],{"class":63,"line":333},[165,471,324],{"class":226},[165,473,475],{"class":63,"line":474},10,[165,476,330],{"class":226},[10,478,479,480,483],{},"Note ",[162,481,482],{},".api"," — that is the escape hatch to the raw COM object model, which is where anything xlwings\nhas not wrapped still lives.",[150,485,487],{"id":486},"job-three-the-file-is-open-on-someones-screen","Job three: the file is open on someone's screen",[10,489,490,491,494,495,499],{},"This is the one that surprises people. openpyxl reads and writes files on disk, so it cannot see\nunsaved changes and cannot save over a workbook Excel has locked — the cause of the\n",[162,492,493],{},"PermissionError"," covered in\n",[14,496,498],{"href":497},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python\u002F","Handle \"Permission Denied\" When Writing Excel in Python",".\nxlwings attaches to the running instance and edits the live book.",[155,501,503],{"className":213,"code":502,"language":215,"meta":160,"style":160},"import xlwings as xw\n\nbook = xw.books.active                       # whatever the user has in front of them\nsheet = book.sheets[\"Input\"]\nsheet.range(\"B2:B10\").value = [[v] for v in values]\nsheet.range(\"B2:B10\").color = (255, 242, 204)\n",[162,504,505,515,519,532,547,573],{"__ignoreMap":160},[165,506,507,509,511,513],{"class":63,"line":167},[165,508,223],{"class":222},[165,510,227],{"class":226},[165,512,230],{"class":222},[165,514,233],{"class":226},[165,516,517],{"class":63,"line":236},[165,518,240],{"emptyLinePlaceholder":239},[165,520,521,524,526,529],{"class":63,"line":243},[165,522,523],{"class":226},"book ",[165,525,256],{"class":222},[165,527,528],{"class":226}," xw.books.active                       ",[165,530,531],{"class":294},"# whatever the user has in front of them\n",[165,533,534,537,539,541,544],{"class":63,"line":271},[165,535,536],{"class":226},"sheet ",[165,538,256],{"class":222},[165,540,306],{"class":226},[165,542,543],{"class":174},"\"Input\"",[165,545,546],{"class":226},"]\n",[165,548,549,552,555,557,559,562,565,568,570],{"class":63,"line":288},[165,550,551],{"class":226},"sheet.range(",[165,553,554],{"class":174},"\"B2:B10\"",[165,556,430],{"class":226},[165,558,256],{"class":222},[165,560,561],{"class":226}," [[v] ",[165,563,564],{"class":222},"for",[165,566,567],{"class":226}," v ",[165,569,446],{"class":222},[165,571,572],{"class":226}," values]\n",[165,574,575,577,579,582,584,587,590,593,596,598,601],{"class":63,"line":298},[165,576,551],{"class":226},[165,578,554],{"class":174},[165,580,581],{"class":226},").color ",[165,583,256],{"class":222},[165,585,586],{"class":226}," (",[165,588,589],{"class":259},"255",[165,591,592],{"class":226},", ",[165,594,595],{"class":259},"242",[165,597,592],{"class":226},[165,599,600],{"class":259},"204",[165,602,285],{"class":226},[10,604,605,606,186],{},"That is also the basis of xlwings' interactive uses — a button in a workbook that calls a Python\nfunction, or a user-defined function written in Python, both covered in\n",[14,607,609],{"href":608},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fcall-python-from-excel-with-xlwings-udf\u002F","Call Python from Excel with an xlwings UDF",[150,611,613],{"id":612},"job-four-output-only-excel-can-produce","Job four: output only Excel can produce",[10,615,616,617,620],{},"A PDF that paginates exactly as Excel prints it, a chart rendered as an image, a ",[162,618,619],{},".xlsb"," file, a\nprint job with the company's page setup — all of these come from Excel's own output paths.",[155,622,624],{"className":213,"code":623,"language":215,"meta":160,"style":160},"import xlwings as xw\n\nwith xw.App(visible=False) as app:\n    book = app.books.open(\"report.xlsx\")\n    book.sheets[\"Summary\"].to_pdf(\"summary.pdf\")\n    book.close()\n",[162,625,626,636,640,658,671,685],{"__ignoreMap":160},[165,627,628,630,632,634],{"class":63,"line":167},[165,629,223],{"class":222},[165,631,227],{"class":226},[165,633,230],{"class":222},[165,635,233],{"class":226},[165,637,638],{"class":63,"line":236},[165,639,240],{"emptyLinePlaceholder":239},[165,641,642,644,646,648,650,652,654,656],{"class":63,"line":243},[165,643,246],{"class":222},[165,645,249],{"class":226},[165,647,253],{"class":252},[165,649,256],{"class":222},[165,651,260],{"class":259},[165,653,263],{"class":226},[165,655,230],{"class":222},[165,657,268],{"class":226},[165,659,660,662,664,666,669],{"class":63,"line":271},[165,661,274],{"class":226},[165,663,256],{"class":222},[165,665,279],{"class":226},[165,667,668],{"class":174},"\"report.xlsx\"",[165,670,285],{"class":226},[165,672,673,675,677,680,683],{"class":63,"line":288},[165,674,409],{"class":226},[165,676,309],{"class":174},[165,678,679],{"class":226},"].to_pdf(",[165,681,682],{"class":174},"\"summary.pdf\"",[165,684,285],{"class":226},[165,686,687],{"class":63,"line":298},[165,688,330],{"class":226},[10,690,691,695],{},[14,692,694],{"href":693},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python\u002F","Convert an Excel File to PDF with Python","\ncompares that against the LibreOffice route, which is the one to use on a server.",[150,697,699],{"id":698},"check-the-alternative-before-you-commit","Check the alternative before you commit",[10,701,702],{},"Each of the four jobs above has a file-level workaround, and it is worth pricing that workaround\nbefore adding a dependency on installed Excel. Sometimes the workaround is obviously better;\nsometimes it is obviously worse; either way the comparison is quick.",[20,704,29,709,29,712,29,715,29,718,29,727,29,733,29,737,29,741,29,745,29,750,29,752,29,755,29,758,29,763,29,766,29,769,29,772,29,777,29,780,29,783,29,788,29,793,29,796,29,799],{"viewBox":705,"role":23,"ariaLabelledBy":706,"xmlns":27,"style":28},"0 0 760 256",[707,708],"xw-jobs-t","xw-jobs-d",[31,710,711],{"id":707},"The four jobs that justify driving Excel, and their alternatives",[35,713,714],{"id":708},"Recalculating formulas, preserving pivot caches and slicers, editing a workbook that is open on screen, and producing native Excel output each have a file-level alternative that is only sometimes acceptable.",[39,716],{"x":41,"y":41,"width":42,"height":717,"fill":44},"256",[39,719],{"x":720,"y":721,"width":722,"height":723,"rx":724,"fill":725,"stroke":726},"18","30","724","34","8","#e7ebef","var(--line,#cdd5e6)",[56,728,732],{"x":729,"y":730,"style":731},"126.6","52","font-size:12px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","Job",[56,734,736],{"x":735,"y":730,"style":731},"361.9","Why openpyxl cannot",[56,738,740],{"x":739,"y":730,"style":731},"615.3000000000001","The cheaper alternative",[39,742],{"x":720,"y":743,"width":722,"height":723,"rx":724,"fill":744,"stroke":120},"67","#f0f4ff",[56,746,749],{"x":729,"y":747,"style":748},"88.0","font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Recalculate",[56,751,81],{"x":735,"y":747,"style":72},[56,753,754],{"x":739,"y":747,"style":72},"compute it in pandas",[39,756],{"x":720,"y":757,"width":722,"height":723,"rx":724,"fill":52,"stroke":53},"107",[56,759,762],{"x":729,"y":760,"style":761},"128.0","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Keep pivot caches",[56,764,765],{"x":735,"y":760,"style":72},"not round-tripped",[56,767,768],{"x":739,"y":760,"style":72},"rebuild the sheet",[39,770],{"x":720,"y":771,"width":722,"height":723,"rx":724,"fill":89,"stroke":90},"147",[56,773,776],{"x":729,"y":774,"style":775},"168.0","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Edit a live file",[56,778,779],{"x":735,"y":774,"style":72},"reads from disk",[56,781,782],{"x":739,"y":774,"style":72},"ask the user to close it",[39,784],{"x":720,"y":785,"width":722,"height":723,"rx":724,"fill":786,"stroke":787},"187","#fee8f2","var(--accent,#d81b73)",[56,789,792],{"x":729,"y":790,"style":791},"208.0","font-size:12px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:middle","Native PDF export",[56,794,795],{"x":735,"y":790,"style":72},"no print engine",[56,797,798],{"x":739,"y":790,"style":72},"LibreOffice headless",[56,800,802],{"x":123,"y":801,"style":147},"244","each row has an escape hatch — check it before paying for Excel",[10,804,805,806,808],{},"Recalculation is the one most often avoidable. If the formula is ",[162,807,196],{}," and your script\nproduced column B in the first place, computing the total in pandas and writing the number is\nfaster, testable and works on a server. It stops being avoidable when the workbook is a model\nsomebody else owns and its formulas are the deliverable.",[10,810,811,812,186],{},"Native PDF export is the one most often worth replacing. LibreOffice in headless mode converts a\nworkbook without Excel, without a desktop session and without a licence, and the output is close\nenough for most reports — the route taken in\n",[14,813,815],{"href":814},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-to-pdf-on-linux-with-libreoffice\u002F","Convert Excel to PDF on Linux with LibreOffice",[150,817,819],{"id":818},"keeping-the-application-from-outliving-the-script","Keeping the application from outliving the script",[10,821,822],{},"The failure that costs the most time with xlwings is not slowness — it is an EXCEL.EXE process left\nrunning after an exception, holding a lock on the file and, after a few scheduled runs, a\nnoticeable share of the machine's memory. The context manager form closes the application even when\nthe body raises, and it is worth using every time.",[155,824,826],{"className":213,"code":825,"language":215,"meta":160,"style":160},"import xlwings as xw\n\ndef refresh(path):\n    with xw.App(visible=False) as app:          # quits even if the body raises\n        app.display_alerts = False\n        app.screen_updating = False\n        book = app.books.open(path)\n        try:\n            book.app.calculate()\n            book.save()\n        finally:\n            book.close()\n",[162,827,828,838,842,854,877,887,896,906,914,919,924,932],{"__ignoreMap":160},[165,829,830,832,834,836],{"class":63,"line":167},[165,831,223],{"class":222},[165,833,227],{"class":226},[165,835,230],{"class":222},[165,837,233],{"class":226},[165,839,840],{"class":63,"line":236},[165,841,240],{"emptyLinePlaceholder":239},[165,843,844,847,851],{"class":63,"line":243},[165,845,846],{"class":222},"def",[165,848,850],{"class":849},"s_Opv"," refresh",[165,852,853],{"class":226},"(path):\n",[165,855,856,859,861,863,865,867,869,871,874],{"class":63,"line":271},[165,857,858],{"class":222},"    with",[165,860,249],{"class":226},[165,862,253],{"class":252},[165,864,256],{"class":222},[165,866,260],{"class":259},[165,868,263],{"class":226},[165,870,230],{"class":222},[165,872,873],{"class":226}," app:          ",[165,875,876],{"class":294},"# quits even if the body raises\n",[165,878,879,882,884],{"class":63,"line":288},[165,880,881],{"class":226},"        app.display_alerts ",[165,883,256],{"class":222},[165,885,886],{"class":259}," False\n",[165,888,889,892,894],{"class":63,"line":298},[165,890,891],{"class":226},"        app.screen_updating ",[165,893,256],{"class":222},[165,895,886],{"class":259},[165,897,898,901,903],{"class":63,"line":321},[165,899,900],{"class":226},"        book ",[165,902,256],{"class":222},[165,904,905],{"class":226}," app.books.open(path)\n",[165,907,908,911],{"class":63,"line":327},[165,909,910],{"class":222},"        try",[165,912,913],{"class":226},":\n",[165,915,916],{"class":63,"line":333},[165,917,918],{"class":226},"            book.app.calculate()\n",[165,920,921],{"class":63,"line":474},[165,922,923],{"class":226},"            book.save()\n",[165,925,927,930],{"class":63,"line":926},11,[165,928,929],{"class":222},"        finally",[165,931,913],{"class":226},[165,933,935],{"class":63,"line":934},12,[165,936,937],{"class":226},"            book.close()\n",[10,939,940,943,944,186],{},[162,941,942],{},"display_alerts = False"," is the other half. Without it a workbook that wants to ask \"save changes?\"\nwill sit on a dialog nobody can see, and the script hangs rather than fails — the single most\nconfusing behaviour in Excel automation, covered in more depth in\n",[14,945,947],{"href":946},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fhandle-com-errors-and-excel-dialog-prompts-in-python\u002F","Handle COM Errors and Excel Dialog Prompts in Python",[150,949,951],{"id":950},"common-pitfalls","Common pitfalls",[953,954,955,971],"table",{},[956,957,958],"thead",{},[959,960,961,965,968],"tr",{},[962,963,964],"th",{},"Symptom",[962,966,967],{},"Cause",[962,969,970],{},"Fix",[972,973,974,994,1012,1026,1037],"tbody",{},[959,975,976,980,983],{},[977,978,979],"td",{},"The script hangs with no error",[977,981,982],{},"Excel is showing a modal dialog nobody can see",[977,984,985,986,989,990,993],{},"Set ",[162,987,988],{},"app.display_alerts = False",", and prefer ",[162,991,992],{},"with xw.App()"," so it always closes",[959,995,996,999,1002],{},[977,997,998],{},"Orphan EXCEL.EXE processes accumulate",[977,1000,1001],{},"The app was never quit after an exception",[977,1003,1004,1005,1008,1009],{},"Use the context manager, or ",[162,1006,1007],{},"app.quit()"," in a ",[162,1010,1011],{},"finally",[959,1013,1014,1017,1020],{},[977,1015,1016],{},"Writing 20,000 rows takes minutes",[977,1018,1019],{},"One cross-process call per cell",[977,1021,1022,1023],{},"Assign a whole range once: ",[162,1024,1025],{},"sheet.range(\"A2\").value = rows",[959,1027,1028,1031,1034],{},[977,1029,1030],{},"Works locally, fails on the server",[977,1032,1033],{},"No Excel installed, or no interactive session",[977,1035,1036],{},"Use openpyxl or xlsxwriter for anything scheduled",[959,1038,1039,1044,1047],{},[977,1040,1041,1043],{},[162,1042,204],{}," where a formula result should be",[977,1045,1046],{},"The workbook was never calculated",[977,1048,1049,1052],{},[162,1050,1051],{},"book.app.calculate()"," before reading",[150,1054,1056],{"id":1055},"testing-a-script-that-needs-excel","Testing a script that needs Excel",[10,1058,1059],{},"The hidden cost of an application driver is that it makes the script hard to test. A unit test for\nan openpyxl function opens a temporary file and asserts on cell values; a unit test for an xlwings\nfunction needs Excel, a desktop session and several seconds of startup, which rules out running it\nin continuous integration.",[10,1061,1062],{},"The practical answer is to keep the Excel-dependent surface as small as possible. Put the logic in\nplain functions that take and return data, and let the xlwings layer do nothing but move values in\nand out.",[155,1064,1066],{"className":213,"code":1065,"language":215,"meta":160,"style":160},"def summarise(rows):\n    \"\"\"Pure logic — no Excel anywhere, tested in milliseconds.\"\"\"\n    totals = {}\n    for region, revenue in rows:\n        totals[region] = totals.get(region, 0.0) + revenue\n    return sorted(totals.items(), key=lambda pair: -pair[1])\n\n\ndef write_summary(book):\n    \"\"\"The only part that needs Excel.\"\"\"\n    rows = book.sheets[\"Raw\"].range(\"A2\").expand().value\n    book.sheets[\"Summary\"].range(\"A2\").value = summarise([(r[0], r[2]) for r in rows])\n",[162,1067,1068,1078,1083,1093,1105,1126,1158,1162,1166,1176,1181,1201],{"__ignoreMap":160},[165,1069,1070,1072,1075],{"class":63,"line":167},[165,1071,846],{"class":222},[165,1073,1074],{"class":849}," summarise",[165,1076,1077],{"class":226},"(rows):\n",[165,1079,1080],{"class":63,"line":236},[165,1081,1082],{"class":174},"    \"\"\"Pure logic — no Excel anywhere, tested in milliseconds.\"\"\"\n",[165,1084,1085,1088,1090],{"class":63,"line":243},[165,1086,1087],{"class":226},"    totals ",[165,1089,256],{"class":222},[165,1091,1092],{"class":226}," {}\n",[165,1094,1095,1097,1100,1102],{"class":63,"line":271},[165,1096,440],{"class":222},[165,1098,1099],{"class":226}," region, revenue ",[165,1101,446],{"class":222},[165,1103,1104],{"class":226}," rows:\n",[165,1106,1107,1110,1112,1115,1118,1120,1123],{"class":63,"line":288},[165,1108,1109],{"class":226},"        totals[region] ",[165,1111,256],{"class":222},[165,1113,1114],{"class":226}," totals.get(region, ",[165,1116,1117],{"class":259},"0.0",[165,1119,263],{"class":226},[165,1121,1122],{"class":222},"+",[165,1124,1125],{"class":226}," revenue\n",[165,1127,1128,1131,1134,1137,1140,1143,1146,1149,1152,1155],{"class":63,"line":298},[165,1129,1130],{"class":222},"    return",[165,1132,1133],{"class":259}," sorted",[165,1135,1136],{"class":226},"(totals.items(), ",[165,1138,1139],{"class":252},"key",[165,1141,1142],{"class":222},"=lambda",[165,1144,1145],{"class":226}," pair: ",[165,1147,1148],{"class":222},"-",[165,1150,1151],{"class":226},"pair[",[165,1153,1154],{"class":259},"1",[165,1156,1157],{"class":226},"])\n",[165,1159,1160],{"class":63,"line":321},[165,1161,240],{"emptyLinePlaceholder":239},[165,1163,1164],{"class":63,"line":327},[165,1165,240],{"emptyLinePlaceholder":239},[165,1167,1168,1170,1173],{"class":63,"line":333},[165,1169,846],{"class":222},[165,1171,1172],{"class":849}," write_summary",[165,1174,1175],{"class":226},"(book):\n",[165,1177,1178],{"class":63,"line":474},[165,1179,1180],{"class":174},"    \"\"\"The only part that needs Excel.\"\"\"\n",[165,1182,1183,1186,1188,1190,1193,1195,1198],{"class":63,"line":926},[165,1184,1185],{"class":226},"    rows ",[165,1187,256],{"class":222},[165,1189,306],{"class":226},[165,1191,1192],{"class":174},"\"Raw\"",[165,1194,312],{"class":226},[165,1196,1197],{"class":174},"\"A2\"",[165,1199,1200],{"class":226},").expand().value\n",[165,1202,1203,1205,1207,1209,1211,1213,1215,1218,1220,1223,1226,1229,1231,1234,1236],{"class":63,"line":934},[165,1204,409],{"class":226},[165,1206,309],{"class":174},[165,1208,312],{"class":226},[165,1210,1197],{"class":174},[165,1212,430],{"class":226},[165,1214,256],{"class":222},[165,1216,1217],{"class":226}," summarise([(r[",[165,1219,41],{"class":259},[165,1221,1222],{"class":226},"], r[",[165,1224,1225],{"class":259},"2",[165,1227,1228],{"class":226},"]) ",[165,1230,564],{"class":222},[165,1232,1233],{"class":226}," r ",[165,1235,446],{"class":222},[165,1237,1238],{"class":226}," rows])\n",[10,1240,1241,1244,1245,1248,1249,1253],{},[162,1242,1243],{},"summarise"," is testable anywhere; ",[162,1246,1247],{},"write_summary"," is three lines that a smoke test can cover on a\ndeveloper machine. That split is worth making even in scripts that will never have a test suite,\nbecause it is also the split that lets you swap xlwings for openpyxl later without rewriting the\npart that holds the business rules. ",[14,1250,1252],{"href":1251},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest","\ncovers the assertions worth making on the file itself.",[150,1255,1257],{"id":1256},"performance-and-scale","Performance and scale",[20,1259,29,1265,29,1268,29,1271,29,1275,29,1280,29,1287,29,1294,29,1300,29,1305,29,1308,29,1311,29,1316,29,1320,29,1322,29,1325,29,1330,29,1334],{"viewBox":1260,"role":23,"ariaLabelledBy":1261,"xmlns":27,"style":1264},"0 0 720 196",[1262,1263],"xw-calls-t","xw-calls-d","width:100%;max-width:720px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,1266,1267],{"id":1262},"Time is driven by boundary crossings, not by data volume",[35,1269,1270],{"id":1263},"Writing ten thousand cells one at a time makes ten thousand cross-process calls and is far slower than assigning the same block in a single range write.",[39,1272],{"x":41,"y":41,"width":1273,"height":1274,"fill":44},"720","196",[56,1276,1279],{"x":47,"y":1277,"style":1278},"56","font-size:11.5px;font-weight:600;fill:var(--text,#172033);text-anchor:start","10,000 single-cell writes",[39,1281],{"x":1282,"y":1283,"width":1284,"height":1285,"rx":1286,"fill":725,"stroke":726},"200","40","371.6","26","6",[39,1288],{"x":1289,"y":1290,"width":1291,"height":1292,"rx":1293,"fill":786,"stroke":787},"201","41","369.6","24","5",[56,1295,1299],{"x":1296,"y":1297,"style":1298},"583.6","58","font-size:12px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:start","10,000 crossings",[56,1301,1304],{"x":47,"y":1302,"style":1303},"100","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start","one range assignment",[39,1306],{"x":1282,"y":1307,"width":1284,"height":1285,"rx":1286,"fill":725,"stroke":726},"84",[39,1309],{"x":1289,"y":1310,"width":48,"height":1292,"rx":1293,"fill":52,"stroke":53},"85",[56,1312,1315],{"x":1296,"y":1313,"style":1314},"102","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","1 crossing",[56,1317,1319],{"x":47,"y":1318,"style":1303},"144","xlsxwriter, same data",[39,1321],{"x":1282,"y":116,"width":1284,"height":1285,"rx":1286,"fill":725,"stroke":726},[39,1323],{"x":1289,"y":1324,"width":48,"height":1292,"rx":1293,"fill":89,"stroke":90},"129",[56,1326,1329],{"x":1296,"y":1327,"style":1328},"146","font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","no Excel at all",[56,1331,1333],{"x":47,"y":47,"style":1332},"font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","relative cost",[56,1335,1338],{"x":1336,"y":1337,"style":147},"360.0","186","a slow xlwings script is nearly always a loop that should be one assignment",[10,1340,1341],{},"The cost model is entirely about the number of boundary crossings, not the amount of data. Writing\none cell and writing a 10,000-cell block cost roughly the same, because the expensive part is the\ncall itself. Any xlwings script that is slow is almost always a loop that should have been a single\nrange assignment.",[155,1343,1345],{"className":213,"code":1344,"language":215,"meta":160,"style":160},"# Slow: 10,000 round trips into Excel.\nfor index, value in enumerate(values, start=2):\n    sheet.range(f\"B{index}\").value = value\n\n# Fast: one round trip.\nsheet.range(\"B2\").value = [[value] for value in values]\n",[162,1346,1347,1352,1377,1407,1411,1416],{"__ignoreMap":160},[165,1348,1349],{"class":63,"line":167},[165,1350,1351],{"class":294},"# Slow: 10,000 round trips into Excel.\n",[165,1353,1354,1356,1359,1361,1364,1367,1370,1372,1374],{"class":63,"line":236},[165,1355,564],{"class":222},[165,1357,1358],{"class":226}," index, value ",[165,1360,446],{"class":222},[165,1362,1363],{"class":259}," enumerate",[165,1365,1366],{"class":226},"(values, ",[165,1368,1369],{"class":252},"start",[165,1371,256],{"class":222},[165,1373,1225],{"class":259},[165,1375,1376],{"class":226},"):\n",[165,1378,1379,1382,1385,1388,1392,1394,1397,1400,1402,1404],{"class":63,"line":243},[165,1380,1381],{"class":226},"    sheet.range(",[165,1383,1384],{"class":222},"f",[165,1386,1387],{"class":174},"\"B",[165,1389,1391],{"class":1390},"sSjpA","{",[165,1393,423],{"class":226},[165,1395,1396],{"class":1390},"}",[165,1398,1399],{"class":174},"\"",[165,1401,430],{"class":226},[165,1403,256],{"class":222},[165,1405,1406],{"class":226}," value\n",[165,1408,1409],{"class":63,"line":271},[165,1410,240],{"emptyLinePlaceholder":239},[165,1412,1413],{"class":63,"line":288},[165,1414,1415],{"class":294},"# Fast: one round trip.\n",[165,1417,1418,1420,1423,1425,1427,1430,1432,1435,1437],{"class":63,"line":298},[165,1419,551],{"class":226},[165,1421,1422],{"class":174},"\"B2\"",[165,1424,430],{"class":226},[165,1426,256],{"class":222},[165,1428,1429],{"class":226}," [[value] ",[165,1431,564],{"class":222},[165,1433,1434],{"class":226}," value ",[165,1436,446],{"class":222},[165,1438,572],{"class":226},[10,1440,1441,1442,186],{},"The other scale limit is operational rather than technical: Excel automation on a server is\nunsupported by Microsoft, needs an interactive desktop session, and breaks in ways that are hard to\ndiagnose remotely. For anything that runs on a schedule, keep the file-level libraries — the\nargument made in ",[14,1443,1445],{"href":1444},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron",[150,1447,1449],{"id":1448},"conclusion","Conclusion",[10,1451,1452],{},"Reach for xlwings when the value you need comes from Excel itself: recalculated formulas, objects\nopenpyxl cannot round-trip, a workbook that is open on someone's screen, or output only Excel can\nproduce. Everything else — bulk reads, bulk writes, formatting, scheduled jobs, anything on Linux —\nbelongs to openpyxl and xlsxwriter, which are faster, testable and free of the requirement that a\nlicensed copy of Excel be sitting on the machine.",[150,1454,1456],{"id":1455},"frequently-asked-questions","Frequently asked questions",[10,1458,1459,1463],{},[1460,1461,1462],"strong",{},"Does xlwings work on Linux?","\nNo. It drives a real copy of Excel through COM on Windows or AppleScript on macOS, so a headless Linux container cannot run it. openpyxl and xlsxwriter are the cross-platform options.",[10,1465,1466,1469],{},[1460,1467,1468],{},"Is xlwings slower than openpyxl?","\nPer operation, dramatically — every read or write is a cross-process call into Excel. Move whole ranges in one assignment rather than looping over cells and the difference becomes manageable, but for bulk writing a file-level library will always win.",[10,1471,1472,1475],{},[1460,1473,1474],{},"Can xlwings run without Excel visible?","\nIt can run with the application hidden (app.visible = False), but Excel must still be installed and licensed. There is no headless mode in the sense a server operator would mean.",[10,1477,1478,1481],{},[1460,1479,1480],{},"What happens to my script if the user has the file open?","\nThat is the case xlwings handles best. It attaches to the open workbook and edits it live, where openpyxl would either read a stale copy from disk or fail to save over a locked file.",[150,1483,1485],{"id":1484},"related","Related",[1487,1488,1489,1496,1503,1508,1515],"ul",{},[1490,1491,1492,1493,1495],"li",{},"Up one level: ",[14,1494,17],{"href":16}," — where xlwings sits among the file-level libraries.",[1490,1497,1498,1502],{},[14,1499,1501],{"href":1500},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002F","Automating Excel with xlwings Basics"," — the API in depth, from ranges to macros.",[1490,1504,1505,1507],{},[14,1506,185],{"href":184}," — the raw interface xlwings wraps on Windows.",[1490,1509,1510,1514],{},[14,1511,1513],{"href":1512},"\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"," — working against the copy a user has open.",[1490,1516,1517,1521],{},[14,1518,1520],{"href":1519},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002F","Using openpyxl for Excel File Manipulation"," — the cross-platform alternative for everything else.",[1523,1524,1525],"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 .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}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_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":160,"searchDepth":236,"depth":236,"links":1527},[1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540],{"id":152,"depth":236,"text":153},{"id":189,"depth":236,"text":190},{"id":350,"depth":236,"text":351},{"id":486,"depth":236,"text":487},{"id":612,"depth":236,"text":613},{"id":698,"depth":236,"text":699},{"id":818,"depth":236,"text":819},{"id":950,"depth":236,"text":951},{"id":1055,"depth":236,"text":1056},{"id":1256,"depth":236,"text":1257},{"id":1448,"depth":236,"text":1449},{"id":1455,"depth":236,"text":1456},{"id":1484,"depth":236,"text":1485},"2026-09-04","xlwings drives a real copy of Excel; openpyxl edits the file. Four jobs justify the cost — recalculation, objects openpyxl drops, live workbooks and native output.","md",[1545,1547,1549,1551],{"q":1462,"a":1546},"No. It drives a real copy of Excel through COM on Windows or AppleScript on macOS, so a headless Linux container cannot run it. openpyxl and xlsxwriter are the cross-platform options.",{"q":1468,"a":1548},"Per operation, dramatically — every read or write is a cross-process call into Excel. Move whole ranges in one assignment rather than looping over cells and the difference becomes manageable, but for bulk writing a file-level library will always win.",{"q":1474,"a":1550},"It can run with the application hidden (app.visible = False), but Excel must still be installed and licensed. There is no headless mode in the sense a server operator would mean.",{"q":1480,"a":1552},"That is the case xlwings handles best. It attaches to the open workbook and edits it live, where openpyxl would either read a stale copy from disk or fail to save over a locked file.",{"breadcrumb":1554},[1555,1558,1561],{"name":1556,"item":1557},"Home","\u002F",{"name":1559,"item":1560},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":17,"item":16},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fwhen-to-use-xlwings-instead-of-openpyxl",{"title":5,"description":1564},"The four jobs that need a running copy of Excel — recalculated formulas, pivot caches and slicers, a workbook open on screen, and native PDF output — and the cost of each.","when-to-use-xlwings-instead-of-openpyxl","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fwhen-to-use-xlwings-instead-of-openpyxl\u002Findex","how-to","Ma4RkUMNlCSlKoavOzdv-hnXuJWjku79emFPp-BXNa4",[1570,1574],{"title":1571,"path":1572,"stem":1573,"children":-1},"Pick an Excel Engine for .xlsx, .xlsm, .xls, .xlsb and .ods","\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods","getting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fpick-an-excel-engine-for-xlsx-xlsm-xls-xlsb-and-ods\u002Findex",{"title":1575,"path":1576,"stem":1577,"children":-1},"Handling Excel File Formats and Conversions","\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions","getting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Findex",1788710154704]