[{"data":1,"prerenderedAt":1600},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fxlwings-vs-pywin32-for-excel-automation":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fxlwings-vs-pywin32-for-excel-automation":1591},{"id":4,"title":5,"body":6,"dateModified":1563,"datePublished":1563,"description":1564,"extension":1565,"faq":1566,"meta":1575,"navigation":191,"path":1584,"seo":1585,"slug":1587,"stem":1588,"type":1589,"__hash__":1590},"docs\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fxlwings-vs-pywin32-for-excel-automation\u002Findex.md","xlwings vs pywin32 for Excel Automation",{"type":7,"value":8,"toc":1548},"minimark",[9,19,112,117,146,149,153,156,543,719,722,726,802,830,892,896,899,910,975,978,982,988,1094,1097,1101,1108,1125,1129,1142,1218,1232,1238,1242,1254,1261,1265,1371,1375,1455,1458,1465,1469,1475,1479,1486,1492,1498,1504,1508,1544],[10,11,12,13,18],"p",{},"xlwings and pywin32 reach the same Excel through the same interface, so the choice between them is\nabout ergonomics rather than capability. That makes it an easy decision most of the time and a\ngenuinely close one in a few cases. This guide, part of\n",[14,15,17],"a",{"href":16},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002F","Automating Excel with COM and pywin32",",\ncompares them on the operations a reporting script actually performs.",[20,21,29,30,29,34,29,38,29,45,29,55,29,62,29,67,29,72,29,77,29,80,29,85,29,90,29,93,29,98,29,103,29,106],"svg",{"viewBox":22,"role":23,"ariaLabelledBy":24,"xmlns":27,"style":28},"0 0 740 320","img",[25,26],"xvp-stack-t","xvp-stack-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:740px;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},"Both libraries reach the same Excel",[35,36,37],"desc",{"id":26},"A script written with either library ends up calling the same COM interface into the same Excel process; xlwings adds a convenience layer above pywin32 and exposes it again through the api attribute.",[39,40],"rect",{"x":41,"y":41,"width":42,"height":43,"fill":44},"0","740","320","#ffffff",[39,46],{"x":47,"y":48,"width":49,"height":50,"rx":51,"fill":52,"stroke":53,"style":54},"26","28","688","56","10","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[56,57,61],"text",{"x":58,"y":59,"style":60},"48","62","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:start","your script",[56,63,66],{"x":64,"y":59,"style":65},"692","font-size:11.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:end","xlwings or pywin32",[39,68],{"x":47,"y":69,"width":49,"height":50,"rx":51,"fill":70,"stroke":71,"style":54},"96","#d9f4f1","var(--teal,#0f9488)",[56,73,76],{"x":58,"y":74,"style":75},"130","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","xlwings",[56,78,79],{"x":64,"y":74,"style":65},"optional convenience layer",[39,81],{"x":47,"y":82,"width":49,"height":50,"rx":51,"fill":83,"stroke":84,"style":54},"164","#fdefd8","var(--gold,#b4740a)",[56,86,89],{"x":58,"y":87,"style":88},"198","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:start","pywin32 \u002F COM",[56,91,92],{"x":64,"y":87,"style":65},"the actual interface",[39,94],{"x":47,"y":95,"width":49,"height":50,"rx":51,"fill":96,"stroke":97,"style":54},"232","#e7ebef","var(--line,#cdd5e6)",[56,99,102],{"x":58,"y":100,"style":101},"266","font-size:13.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:start","EXCEL.EXE",[56,104,105],{"x":64,"y":100,"style":65},"identical either way",[56,107,111],{"x":108,"y":109,"style":110},"370.0","308","font-size:12.5px;font-weight:400;fill:var(--muted,#5b6780);text-anchor:middle","the capability is the same; the ergonomics are not",[113,114,116],"h2",{"id":115},"prerequisites","Prerequisites",[118,119,124],"pre",{"className":120,"code":121,"language":122,"meta":123,"style":123},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install xlwings pywin32\n","bash","",[125,126,127],"code",{"__ignoreMap":123},[128,129,132,136,140,143],"span",{"class":130,"line":131},"line",1,[128,133,135],{"class":134},"sMTad","pip",[128,137,139],{"class":138},"srMev"," install",[128,141,142],{"class":138}," xlwings",[128,144,145],{"class":138}," pywin32\n",[10,147,148],{},"Windows with Excel installed. xlwings also runs on macOS; pywin32 does not.",[113,150,152],{"id":151},"the-same-job-written-twice","The same job, written twice",[10,154,155],{},"Nothing shows the difference faster than one task in both libraries. Here is opening a workbook,\nreading a block, summarising it and writing the result back.",[118,157,161],{"className":158,"code":159,"language":160,"meta":123,"style":123},"language-python shiki shiki-themes github-light github-dark-high-contrast","# pywin32\nimport win32com.client as win32\n\nexcel = win32.DispatchEx(\"Excel.Application\")\nexcel.Visible = False\nexcel.DisplayAlerts = False\ntry:\n    book = excel.Workbooks.Open(r\"C:\\data\\orders.xlsx\")\n    try:\n        sheet = book.Sheets(\"Data\")\n        last = sheet.Cells(sheet.Rows.Count, 1).End(-4162).Row\n        rows = sheet.Range(sheet.Cells(2, 1), sheet.Cells(last, 4)).Value\n        totals = {}\n        for row in rows:\n            totals[row[1]] = totals.get(row[1], 0) + row[3]\n        out = book.Sheets(\"Summary\")\n        out.Range(out.Cells(2, 1), out.Cells(len(totals) + 1, 2)).Value = [\n            [k, v] for k, v in totals.items()\n        ]\n        book.Save()\n    finally:\n        book.Close(SaveChanges=False)\nfinally:\n    excel.Quit()\n","python",[125,162,163,169,186,193,211,223,233,242,280,288,304,330,358,369,384,422,437,475,492,498,504,512,529,537],{"__ignoreMap":123},[128,164,165],{"class":130,"line":131},[128,166,168],{"class":167},"s-wDw","# pywin32\n",[128,170,172,176,180,183],{"class":130,"line":171},2,[128,173,175],{"class":174},"s-kum","import",[128,177,179],{"class":178},"skGVy"," win32com.client ",[128,181,182],{"class":174},"as",[128,184,185],{"class":178}," win32\n",[128,187,189],{"class":130,"line":188},3,[128,190,192],{"emptyLinePlaceholder":191},true,"\n",[128,194,196,199,202,205,208],{"class":130,"line":195},4,[128,197,198],{"class":178},"excel ",[128,200,201],{"class":174},"=",[128,203,204],{"class":178}," win32.DispatchEx(",[128,206,207],{"class":138},"\"Excel.Application\"",[128,209,210],{"class":178},")\n",[128,212,214,217,219],{"class":130,"line":213},5,[128,215,216],{"class":178},"excel.Visible ",[128,218,201],{"class":174},[128,220,222],{"class":221},"sP0c6"," False\n",[128,224,226,229,231],{"class":130,"line":225},6,[128,227,228],{"class":178},"excel.DisplayAlerts ",[128,230,201],{"class":174},[128,232,222],{"class":221},[128,234,236,239],{"class":130,"line":235},7,[128,237,238],{"class":174},"try",[128,240,241],{"class":178},":\n",[128,243,245,248,250,253,256,259,262,265,269,272,275,278],{"class":130,"line":244},8,[128,246,247],{"class":178},"    book ",[128,249,201],{"class":174},[128,251,252],{"class":178}," excel.Workbooks.Open(",[128,254,255],{"class":174},"r",[128,257,258],{"class":138},"\"C:",[128,260,261],{"class":221},"\\d",[128,263,264],{"class":138},"ata",[128,266,268],{"class":267},"s_b0D","\\o",[128,270,271],{"class":138},"rders",[128,273,274],{"class":221},".",[128,276,277],{"class":138},"xlsx\"",[128,279,210],{"class":178},[128,281,283,286],{"class":130,"line":282},9,[128,284,285],{"class":174},"    try",[128,287,241],{"class":178},[128,289,291,294,296,299,302],{"class":130,"line":290},10,[128,292,293],{"class":178},"        sheet ",[128,295,201],{"class":174},[128,297,298],{"class":178}," book.Sheets(",[128,300,301],{"class":138},"\"Data\"",[128,303,210],{"class":178},[128,305,307,310,312,315,318,321,324,327],{"class":130,"line":306},11,[128,308,309],{"class":178},"        last ",[128,311,201],{"class":174},[128,313,314],{"class":178}," sheet.Cells(sheet.Rows.Count, ",[128,316,317],{"class":221},"1",[128,319,320],{"class":178},").End(",[128,322,323],{"class":174},"-",[128,325,326],{"class":221},"4162",[128,328,329],{"class":178},").Row\n",[128,331,333,336,338,341,344,347,349,352,355],{"class":130,"line":332},12,[128,334,335],{"class":178},"        rows ",[128,337,201],{"class":174},[128,339,340],{"class":178}," sheet.Range(sheet.Cells(",[128,342,343],{"class":221},"2",[128,345,346],{"class":178},", ",[128,348,317],{"class":221},[128,350,351],{"class":178},"), sheet.Cells(last, ",[128,353,354],{"class":221},"4",[128,356,357],{"class":178},")).Value\n",[128,359,361,364,366],{"class":130,"line":360},13,[128,362,363],{"class":178},"        totals ",[128,365,201],{"class":174},[128,367,368],{"class":178}," {}\n",[128,370,372,375,378,381],{"class":130,"line":371},14,[128,373,374],{"class":174},"        for",[128,376,377],{"class":178}," row ",[128,379,380],{"class":174},"in",[128,382,383],{"class":178}," rows:\n",[128,385,387,390,392,395,397,400,402,405,407,410,413,416,419],{"class":130,"line":386},15,[128,388,389],{"class":178},"            totals[row[",[128,391,317],{"class":221},[128,393,394],{"class":178},"]] ",[128,396,201],{"class":174},[128,398,399],{"class":178}," totals.get(row[",[128,401,317],{"class":221},[128,403,404],{"class":178},"], ",[128,406,41],{"class":221},[128,408,409],{"class":178},") ",[128,411,412],{"class":174},"+",[128,414,415],{"class":178}," row[",[128,417,418],{"class":221},"3",[128,420,421],{"class":178},"]\n",[128,423,425,428,430,432,435],{"class":130,"line":424},16,[128,426,427],{"class":178},"        out ",[128,429,201],{"class":174},[128,431,298],{"class":178},[128,433,434],{"class":138},"\"Summary\"",[128,436,210],{"class":178},[128,438,440,443,445,447,449,452,455,458,460,463,465,467,470,472],{"class":130,"line":439},17,[128,441,442],{"class":178},"        out.Range(out.Cells(",[128,444,343],{"class":221},[128,446,346],{"class":178},[128,448,317],{"class":221},[128,450,451],{"class":178},"), out.Cells(",[128,453,454],{"class":221},"len",[128,456,457],{"class":178},"(totals) ",[128,459,412],{"class":174},[128,461,462],{"class":221}," 1",[128,464,346],{"class":178},[128,466,343],{"class":221},[128,468,469],{"class":178},")).Value ",[128,471,201],{"class":174},[128,473,474],{"class":178}," [\n",[128,476,478,481,484,487,489],{"class":130,"line":477},18,[128,479,480],{"class":178},"            [k, v] ",[128,482,483],{"class":174},"for",[128,485,486],{"class":178}," k, v ",[128,488,380],{"class":174},[128,490,491],{"class":178}," totals.items()\n",[128,493,495],{"class":130,"line":494},19,[128,496,497],{"class":178},"        ]\n",[128,499,501],{"class":130,"line":500},20,[128,502,503],{"class":178},"        book.Save()\n",[128,505,507,510],{"class":130,"line":506},21,[128,508,509],{"class":174},"    finally",[128,511,241],{"class":178},[128,513,515,518,522,524,527],{"class":130,"line":514},22,[128,516,517],{"class":178},"        book.Close(",[128,519,521],{"class":520},"sa561","SaveChanges",[128,523,201],{"class":174},[128,525,526],{"class":221},"False",[128,528,210],{"class":178},[128,530,532,535],{"class":130,"line":531},23,[128,533,534],{"class":174},"finally",[128,536,241],{"class":178},[128,538,540],{"class":130,"line":539},24,[128,541,542],{"class":178},"    excel.Quit()\n",[118,544,546],{"className":158,"code":545,"language":160,"meta":123,"style":123},"# xlwings\nimport xlwings as xw\n\nwith xw.App(visible=False) as app:\n    book = app.books.open(r\"C:\\data\\orders.xlsx\")\n    frame = book.sheets[\"Data\"].range(\"A1\").options(\"df\", expand=\"table\").value\n    summary = frame.groupby(\"Region\", as_index=False)[\"Revenue\"].sum()\n    book.sheets[\"Summary\"].range(\"A1\").options(index=False).value = summary\n    book.save()\n",[125,547,548,553,565,569,591,618,655,686,714],{"__ignoreMap":123},[128,549,550],{"class":130,"line":131},[128,551,552],{"class":167},"# xlwings\n",[128,554,555,557,560,562],{"class":130,"line":171},[128,556,175],{"class":174},[128,558,559],{"class":178}," xlwings ",[128,561,182],{"class":174},[128,563,564],{"class":178}," xw\n",[128,566,567],{"class":130,"line":188},[128,568,192],{"emptyLinePlaceholder":191},[128,570,571,574,577,580,582,584,586,588],{"class":130,"line":195},[128,572,573],{"class":174},"with",[128,575,576],{"class":178}," xw.App(",[128,578,579],{"class":520},"visible",[128,581,201],{"class":174},[128,583,526],{"class":221},[128,585,409],{"class":178},[128,587,182],{"class":174},[128,589,590],{"class":178}," app:\n",[128,592,593,595,597,600,602,604,606,608,610,612,614,616],{"class":130,"line":213},[128,594,247],{"class":178},[128,596,201],{"class":174},[128,598,599],{"class":178}," app.books.open(",[128,601,255],{"class":174},[128,603,258],{"class":138},[128,605,261],{"class":221},[128,607,264],{"class":138},[128,609,268],{"class":267},[128,611,271],{"class":138},[128,613,274],{"class":221},[128,615,277],{"class":138},[128,617,210],{"class":178},[128,619,620,623,625,628,630,633,636,639,642,644,647,649,652],{"class":130,"line":225},[128,621,622],{"class":178},"    frame ",[128,624,201],{"class":174},[128,626,627],{"class":178}," book.sheets[",[128,629,301],{"class":138},[128,631,632],{"class":178},"].range(",[128,634,635],{"class":138},"\"A1\"",[128,637,638],{"class":178},").options(",[128,640,641],{"class":138},"\"df\"",[128,643,346],{"class":178},[128,645,646],{"class":520},"expand",[128,648,201],{"class":174},[128,650,651],{"class":138},"\"table\"",[128,653,654],{"class":178},").value\n",[128,656,657,660,662,665,668,670,673,675,677,680,683],{"class":130,"line":235},[128,658,659],{"class":178},"    summary ",[128,661,201],{"class":174},[128,663,664],{"class":178}," frame.groupby(",[128,666,667],{"class":138},"\"Region\"",[128,669,346],{"class":178},[128,671,672],{"class":520},"as_index",[128,674,201],{"class":174},[128,676,526],{"class":221},[128,678,679],{"class":178},")[",[128,681,682],{"class":138},"\"Revenue\"",[128,684,685],{"class":178},"].sum()\n",[128,687,688,691,693,695,697,699,702,704,706,709,711],{"class":130,"line":244},[128,689,690],{"class":178},"    book.sheets[",[128,692,434],{"class":138},[128,694,632],{"class":178},[128,696,635],{"class":138},[128,698,638],{"class":178},[128,700,701],{"class":520},"index",[128,703,201],{"class":174},[128,705,526],{"class":221},[128,707,708],{"class":178},").value ",[128,710,201],{"class":174},[128,712,713],{"class":178}," summary\n",[128,715,716],{"class":130,"line":282},[128,717,718],{"class":178},"    book.save()\n",[10,720,721],{},"Six lines against twenty-five, and the six do not have a cleanup bug waiting in them. That gap — a\ncontext manager, DataFrame conversion and range expansion, all built in — is the whole argument for\nxlwings.",[113,723,725],{"id":724},"where-the-wrapper-actually-helps","Where the wrapper actually helps",[20,727,29,733,29,736,29,739,29,742,29,747,29,752,29,757,29,762,29,766,29,769,29,773,29,776,29,781,29,785,29,788,29,791,29,794,29,797],{"viewBox":728,"role":23,"ariaLabelledBy":729,"xmlns":27,"style":732},"0 0 760 232",[730,731],"xvp-feat-t","xvp-feat-d","width:100%;max-width:760px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[31,734,735],{"id":730},"What each library gives you for the same task",[35,737,738],{"id":731},"xlwings supplies a context manager, DataFrame conversion, range expansion and Pythonic collections, while pywin32 gives Excel's documented object model, a one-for-one match with recorded VBA and a single dependency.",[39,740],{"x":41,"y":41,"width":741,"height":95,"fill":44},"760",[39,743],{"x":744,"y":47,"width":745,"height":82,"rx":746,"fill":70,"stroke":71,"style":54},"20.0","350.0","14",[56,748,76],{"x":749,"y":750,"style":751},"195.0","52","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[130,753],{"x1":754,"y1":59,"x2":755,"y2":59,"stroke":71,"style":756},"36.0","354.0","stroke-width:1px",[56,758,761],{"x":749,"y":759,"style":760},"84","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","App is a context manager",[56,763,765],{"x":749,"y":764,"style":760},"107","DataFrames both ways",[56,767,768],{"x":749,"y":74,"style":760},"expand='table'",[56,770,772],{"x":749,"y":771,"style":760},"153","iterable collections",[39,774],{"x":775,"y":47,"width":745,"height":82,"rx":746,"fill":52,"stroke":53,"style":54},"390.0",[56,777,780],{"x":778,"y":750,"style":779},"565.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","pywin32",[130,782],{"x1":783,"y1":59,"x2":784,"y2":59,"stroke":53,"style":756},"406.0","724.0",[56,786,787],{"x":778,"y":759,"style":760},"the documented model",[56,789,790],{"x":778,"y":764,"style":760},"matches recorded VBA",[56,792,793],{"x":778,"y":74,"style":760},"one dependency",[56,795,796],{"x":778,"y":771,"style":760},"manual lifecycle",[56,798,801],{"x":799,"y":800,"style":110},"380.0","218","six lines against twenty-five, for the same result",[10,803,804,805,808,809,813,814,817,818,821,822,825,826,829],{},"Four features account for most of the difference. ",[125,806,807],{},"xw.App"," is a context manager, so the application\nquits even when the body raises — the failure mode covered in\n",[14,810,812],{"href":811},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fclose-excel-cleanly-and-avoid-orphan-com-processes\u002F","Close Excel Cleanly and Avoid Orphan COM Processes",".\n",[125,815,816],{},"options(\"df\")"," converts a range to and from a DataFrame in one call rather than fifteen lines of\ntuple juggling. ",[125,819,820],{},"expand=\"table\""," finds the extent of a block without the ",[125,823,824],{},"End(xlUp)"," incantation.\nAnd collections are Pythonic: ",[125,827,828],{},"for sheet in book.sheets"," works, where the COM collection needs\neither an index loop or an awkward iteration.",[118,831,833],{"className":158,"code":832,"language":160,"meta":123,"style":123},"import xlwings as xw\n\nbook = xw.books.active\nfor sheet in book.sheets:                       # ordinary iteration\n    used = sheet.used_range\n    print(sheet.name, used.shape, used.address)\n",[125,834,835,845,849,859,874,884],{"__ignoreMap":123},[128,836,837,839,841,843],{"class":130,"line":131},[128,838,175],{"class":174},[128,840,559],{"class":178},[128,842,182],{"class":174},[128,844,564],{"class":178},[128,846,847],{"class":130,"line":171},[128,848,192],{"emptyLinePlaceholder":191},[128,850,851,854,856],{"class":130,"line":188},[128,852,853],{"class":178},"book ",[128,855,201],{"class":174},[128,857,858],{"class":178}," xw.books.active\n",[128,860,861,863,866,868,871],{"class":130,"line":195},[128,862,483],{"class":174},[128,864,865],{"class":178}," sheet ",[128,867,380],{"class":174},[128,869,870],{"class":178}," book.sheets:                       ",[128,872,873],{"class":167},"# ordinary iteration\n",[128,875,876,879,881],{"class":130,"line":213},[128,877,878],{"class":178},"    used ",[128,880,201],{"class":174},[128,882,883],{"class":178}," sheet.used_range\n",[128,885,886,889],{"class":130,"line":225},[128,887,888],{"class":221},"    print",[128,890,891],{"class":178},"(sheet.name, used.shape, used.address)\n",[113,893,895],{"id":894},"where-pywin32-still-wins","Where pywin32 still wins",[10,897,898],{},"Three situations favour the raw interface. Translating an existing macro is the strongest: the VBA\nyou are porting names COM objects and methods directly, and mapping them onto xlwings' abstractions\nadds a translation step where matching them one for one does not.",[10,900,901,902,905,906,909],{},"Reaching an unwrapped corner of Excel is the second. xlwings covers what most scripts need, but\nmail merge, some chart and shape properties, ",[125,903,904],{},"Application","-level options and several print settings\nare only available through the object model — and reaching them means ",[125,907,908],{},"sheet.api",", which is a\npywin32 object anyway.",[118,911,913],{"className":158,"code":912,"language":160,"meta":123,"style":123},"import xlwings as xw\n\nsheet = xw.books.active.sheets[\"Summary\"]\nsheet.api.PageSetup.Orientation = 2          # xlLandscape, straight through the wrapper\nsheet.api.PageSetup.Zoom = False\nsheet.api.PageSetup.FitToPagesWide = 1\n",[125,914,915,925,929,943,956,965],{"__ignoreMap":123},[128,916,917,919,921,923],{"class":130,"line":131},[128,918,175],{"class":174},[128,920,559],{"class":178},[128,922,182],{"class":174},[128,924,564],{"class":178},[128,926,927],{"class":130,"line":171},[128,928,192],{"emptyLinePlaceholder":191},[128,930,931,934,936,939,941],{"class":130,"line":188},[128,932,933],{"class":178},"sheet ",[128,935,201],{"class":174},[128,937,938],{"class":178}," xw.books.active.sheets[",[128,940,434],{"class":138},[128,942,421],{"class":178},[128,944,945,948,950,953],{"class":130,"line":195},[128,946,947],{"class":178},"sheet.api.PageSetup.Orientation ",[128,949,201],{"class":174},[128,951,952],{"class":221}," 2",[128,954,955],{"class":167},"          # xlLandscape, straight through the wrapper\n",[128,957,958,961,963],{"class":130,"line":213},[128,959,960],{"class":178},"sheet.api.PageSetup.Zoom ",[128,962,201],{"class":174},[128,964,222],{"class":221},[128,966,967,970,972],{"class":130,"line":225},[128,968,969],{"class":178},"sheet.api.PageSetup.FitToPagesWide ",[128,971,201],{"class":174},[128,973,974],{"class":221}," 1\n",[10,976,977],{},"Dependency footprint is the third and least interesting: pywin32 is one package, xlwings is two.\nThat matters on a locked-down machine where every dependency needs approval, and nowhere else.",[113,979,981],{"id":980},"the-escape-hatch-and-why-it-matters","The escape hatch, and why it matters",[10,983,984,987],{},[125,985,986],{},".api"," is the reason this is rarely an either\u002For decision. Every xlwings object exposes the\nunderlying COM object, so a script can use the friendly API for structure and drop to the raw model\nfor the one call that needs it.",[118,989,991],{"className":158,"code":990,"language":160,"meta":123,"style":123},"import xlwings as xw\n\nwith xw.App(visible=False) as app:\n    book = app.books.open(r\"C:\\finance\\model.xlsm\")\n    book.macro(\"RebuildSummary\")(\"2026-Q3\")            # xlwings\n    book.api.RefreshAll()                              # raw COM\n    app.api.CalculateUntilAsyncQueriesDone()           # raw COM\n    book.save()\n",[125,992,993,1003,1007,1025,1056,1075,1083,1090],{"__ignoreMap":123},[128,994,995,997,999,1001],{"class":130,"line":131},[128,996,175],{"class":174},[128,998,559],{"class":178},[128,1000,182],{"class":174},[128,1002,564],{"class":178},[128,1004,1005],{"class":130,"line":171},[128,1006,192],{"emptyLinePlaceholder":191},[128,1008,1009,1011,1013,1015,1017,1019,1021,1023],{"class":130,"line":188},[128,1010,573],{"class":174},[128,1012,576],{"class":178},[128,1014,579],{"class":520},[128,1016,201],{"class":174},[128,1018,526],{"class":221},[128,1020,409],{"class":178},[128,1022,182],{"class":174},[128,1024,590],{"class":178},[128,1026,1027,1029,1031,1033,1035,1037,1040,1043,1046,1049,1051,1054],{"class":130,"line":195},[128,1028,247],{"class":178},[128,1030,201],{"class":174},[128,1032,599],{"class":178},[128,1034,255],{"class":174},[128,1036,258],{"class":138},[128,1038,1039],{"class":267},"\\f",[128,1041,1042],{"class":138},"inance",[128,1044,1045],{"class":267},"\\m",[128,1047,1048],{"class":138},"odel",[128,1050,274],{"class":221},[128,1052,1053],{"class":138},"xlsm\"",[128,1055,210],{"class":178},[128,1057,1058,1061,1064,1067,1070,1073],{"class":130,"line":213},[128,1059,1060],{"class":178},"    book.macro(",[128,1062,1063],{"class":138},"\"RebuildSummary\"",[128,1065,1066],{"class":178},")(",[128,1068,1069],{"class":138},"\"2026-Q3\"",[128,1071,1072],{"class":178},")            ",[128,1074,552],{"class":167},[128,1076,1077,1080],{"class":130,"line":225},[128,1078,1079],{"class":178},"    book.api.RefreshAll()                              ",[128,1081,1082],{"class":167},"# raw COM\n",[128,1084,1085,1088],{"class":130,"line":235},[128,1086,1087],{"class":178},"    app.api.CalculateUntilAsyncQueriesDone()           ",[128,1089,1082],{"class":167},[128,1091,1092],{"class":130,"line":244},[128,1093,718],{"class":178},[10,1095,1096],{},"Learning the COM object model is therefore not wasted effort even if you use xlwings exclusively —\nit is what you fall back on the first time the wrapper does not reach far enough, and it is what the\nrecorded VBA in front of you is written in.",[113,1098,1100],{"id":1099},"portability-and-platform","Portability and platform",[10,1102,1103,1104,274],{},"xlwings runs the same script on Windows and macOS by swapping COM for AppleScript underneath. That\nis genuinely useful for a tool a team runs on their own machines, and irrelevant for a scheduled job\nthat will only ever run on one server. Neither library helps on Linux, where the answer remains the\nfile-level libraries described in\n",[14,1105,1107],{"href":1106},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002F","Choosing a Python Excel Library",[10,1109,1110,1111,1115,1116,1119,1120,1124],{},"xlwings also has a second half that pywin32 has no equivalent of: running Python ",[1112,1113,1114],"em",{},"from"," Excel,\nthrough user-defined functions and the ",[125,1117,1118],{},"RunPython"," macro. If the requirement includes a button in a\nworkbook, xlwings is not merely the friendlier choice but the only one —\n",[14,1121,1123],{"href":1122},"\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","\ncovers that direction.",[113,1126,1128],{"id":1127},"migrating-from-one-to-the-other","Migrating from one to the other",[10,1130,1131,1132,1134,1135,1137,1138,1141],{},"Because both libraries end up at the same object model, moving between them is mechanical rather\nthan a rewrite. Going from pywin32 to xlwings, the translation is largely about deleting: the\nlifecycle boilerplate becomes a ",[125,1133,573],{}," block, the range sizing becomes ",[125,1136,820],{},", and the\ntuple-of-tuples handling becomes an ",[125,1139,1140],{},"options"," call.",[118,1143,1145],{"className":158,"code":1144,"language":160,"meta":123,"style":123},"# pywin32\nlast = sheet.Cells(sheet.Rows.Count, 1).End(-4162).Row\nrows = sheet.Range(sheet.Cells(2, 1), sheet.Cells(last, 4)).Value\n\n# xlwings\nrows = sheet.range(\"A2\").expand(\"table\").value\n",[125,1146,1147,1151,1170,1191,1195,1199],{"__ignoreMap":123},[128,1148,1149],{"class":130,"line":131},[128,1150,168],{"class":167},[128,1152,1153,1156,1158,1160,1162,1164,1166,1168],{"class":130,"line":171},[128,1154,1155],{"class":178},"last ",[128,1157,201],{"class":174},[128,1159,314],{"class":178},[128,1161,317],{"class":221},[128,1163,320],{"class":178},[128,1165,323],{"class":174},[128,1167,326],{"class":221},[128,1169,329],{"class":178},[128,1171,1172,1175,1177,1179,1181,1183,1185,1187,1189],{"class":130,"line":188},[128,1173,1174],{"class":178},"rows ",[128,1176,201],{"class":174},[128,1178,340],{"class":178},[128,1180,343],{"class":221},[128,1182,346],{"class":178},[128,1184,317],{"class":221},[128,1186,351],{"class":178},[128,1188,354],{"class":221},[128,1190,357],{"class":178},[128,1192,1193],{"class":130,"line":195},[128,1194,192],{"emptyLinePlaceholder":191},[128,1196,1197],{"class":130,"line":213},[128,1198,552],{"class":167},[128,1200,1201,1203,1205,1208,1211,1214,1216],{"class":130,"line":225},[128,1202,1174],{"class":178},[128,1204,201],{"class":174},[128,1206,1207],{"class":178}," sheet.range(",[128,1209,1210],{"class":138},"\"A2\"",[128,1212,1213],{"class":178},").expand(",[128,1215,651],{"class":138},[128,1217,654],{"class":178},[10,1219,1220,1221,1224,1225,1228,1229,1231],{},"The other direction is just as mechanical, but the checklist is longer because the boilerplate has\nto come back. Every ",[125,1222,1223],{},"with xw.App(...)"," becomes an explicit try\u002Ffinally with a ",[125,1226,1227],{},"Quit",", every\n",[125,1230,816],{}," becomes a manual header-plus-body construction, and every collection iteration\nbecomes an index loop. It is worth doing only when one of the three reasons above genuinely applies,\nbecause none of the reintroduced code adds capability.",[10,1233,1234,1235,1237],{},"A useful intermediate exists: keep xlwings for the session and write the operations against ",[125,1236,986],{},".\nThat gives the context manager and the platform handling while the calls themselves read like the\nVBA they came from, which makes it a good landing point when porting a large macro incrementally.",[113,1239,1241],{"id":1240},"choosing-for-a-team-rather-than-a-script","Choosing for a team rather than a script",[10,1243,1244,1245,346,1247,346,1250,1253],{},"The last consideration is not technical. A script written with xlwings can be read by somebody who\nknows Python and has never seen Excel's object model; a pywin32 script assumes familiarity with\n",[125,1246,904],{},[125,1248,1249],{},"Workbooks",[125,1251,1252],{},"Range"," and the constants, and reads like VBA transliterated. On a team\nwhere the Excel automation is maintained by whoever is available rather than by a specialist, that\ndifference in legibility outweighs almost everything else in this comparison.",[10,1255,1256,1257,274],{},"The counterweight is that the object model is the shared vocabulary between Python and every macro\nalready in the organisation. A team maintaining a large body of VBA has that knowledge anyway, and\nfor them the raw interface is the one that matches what they already read. Deciding on that basis —\nwho maintains this in a year — tends to produce a better answer than comparing feature lists, and it\nis the same reasoning behind the packaging choices in\n",[14,1258,1260],{"href":1259},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",[113,1262,1264],{"id":1263},"common-pitfalls","Common pitfalls",[1266,1267,1268,1284],"table",{},[1269,1270,1271],"thead",{},[1272,1273,1274,1278,1281],"tr",{},[1275,1276,1277],"th",{},"Symptom",[1275,1279,1280],{},"Cause",[1275,1282,1283],{},"Fix",[1285,1286,1287,1302,1320,1337,1354],"tbody",{},[1272,1288,1289,1293,1296],{},[1290,1291,1292],"td",{},"An xlwings method does not exist",[1290,1294,1295],{},"The feature is not wrapped",[1290,1297,1298,1299,1301],{},"Use ",[125,1300,986],{}," and call the COM member directly",[1272,1303,1304,1311,1314],{},[1290,1305,1306,1307,1310],{},"Constants like ",[125,1308,1309],{},"xlLandscape"," are undefined in xlwings",[1290,1312,1313],{},"It does not re-export the type library",[1290,1315,1316,1317],{},"Use the integer, or ",[125,1318,1319],{},"win32com.client.constants",[1272,1321,1322,1325,1331],{},[1290,1323,1324],{},"Scripts differ between Windows and macOS",[1290,1326,1327,1328,1330],{},"Raw ",[125,1329,986],{}," calls are platform-specific",[1290,1332,1333,1334,1336],{},"Keep ",[125,1335,986],{}," use behind a small platform check",[1272,1338,1339,1345,1348],{},[1290,1340,1341,1344],{},[125,1342,1343],{},"App"," context manager closes a user's Excel",[1290,1346,1347],{},"It started a new instance but another was attached",[1290,1349,1298,1350,1353],{},[125,1351,1352],{},"xw.apps.active"," deliberately when acting on a live session",[1272,1355,1356,1359,1362],{},[1290,1357,1358],{},"Mixed libraries fight over the same instance",[1290,1360,1361],{},"Two applications started independently",[1290,1363,1364,1365,1367,1368,1370],{},"Get one ",[125,1366,1343],{}," and pass its ",[125,1369,986],{}," around",[113,1372,1374],{"id":1373},"performance-and-scale","Performance and scale",[20,1376,29,1382,29,1385,29,1388,29,1392,29,1397,29,1403,29,1412,29,1418,29,1422,29,1424,29,1428,29,1431,29,1435,29,1438,29,1441,29,1446,29,1450],{"viewBox":1377,"role":23,"ariaLabelledBy":1378,"xmlns":27,"style":1381},"0 0 720 196",[1379,1380],"xvp-cost-t","xvp-cost-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,1383,1384],{"id":1379},"Neither wrapper changes the cost model",[35,1386,1387],{"id":1380},"Both libraries pay per boundary crossing, so a block read costs the same either way and a per-cell loop is equally slow in both.",[39,1389],{"x":41,"y":41,"width":1390,"height":1391,"fill":44},"720","196",[56,1393,1396],{"x":1394,"y":50,"style":1395},"20","font-size:12px;font-weight:600;fill:var(--text,#172033);text-anchor:start","per-cell loop, pywin32",[39,1398],{"x":1399,"y":1400,"width":1401,"height":47,"rx":1402,"fill":96,"stroke":97},"200","40","371.6","6",[39,1404],{"x":1405,"y":1406,"width":1407,"height":1408,"rx":1409,"fill":1410,"stroke":1411},"201","41","369.6","24","5","#fee8f2","var(--accent,#d81b73)",[56,1413,1417],{"x":1414,"y":1415,"style":1416},"583.6","58","font-size:12px;font-weight:700;fill:var(--accent,#d81b73);text-anchor:start","50,000 crossings",[56,1419,1421],{"x":1394,"y":1420,"style":1395},"100","per-cell loop, xlwings",[39,1423],{"x":1399,"y":759,"width":1401,"height":47,"rx":1402,"fill":96,"stroke":97},[39,1425],{"x":1405,"y":1426,"width":1427,"height":1408,"rx":1409,"fill":1410,"stroke":1411},"85","362.1",[56,1429,1417],{"x":1414,"y":1430,"style":1416},"102",[56,1432,1434],{"x":1394,"y":1433,"style":1395},"144","block read, either",[39,1436],{"x":1399,"y":1437,"width":1401,"height":47,"rx":1402,"fill":96,"stroke":97},"128",[39,1439],{"x":1405,"y":1440,"width":48,"height":1408,"rx":1409,"fill":70,"stroke":71},"129",[56,1442,1445],{"x":1414,"y":1443,"style":1444},"146","font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:start","1 crossing",[56,1447,1449],{"x":1394,"y":1394,"style":1448},"font-size:11.5px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:start","relative cost",[56,1451,1454],{"x":1452,"y":1453,"style":110},"360.0","186","the library is not the variable — the crossings are",[10,1456,1457],{},"Neither library changes the cost model: both pay per boundary crossing, and both are fast when\nranges move in blocks. xlwings' DataFrame conversion is a single crossing plus a local conversion,\nso it is not slower than doing the same thing by hand — and it is frequently faster in practice,\nbecause the built-in path batches correctly where hand-written code often does not.",[10,1459,1460,1461,1464],{},"The one place raw pywin32 can win on performance is when a script needs ",[125,1462,1463],{},"Value2"," semantics or an\nunusual read shape that the wrapper converts unnecessarily. Reading a hundred thousand cells as raw\nserials and converting them yourself avoids a per-cell datetime construction, which is measurable —\nthough it is a narrow enough case that it should be a response to a profile rather than a default.",[113,1466,1468],{"id":1467},"conclusion","Conclusion",[10,1470,1471,1472,1474],{},"Default to xlwings. It removes the cleanup bug, converts DataFrames for free, and keeps the script\nreadable, while ",[125,1473,986],{}," means nothing is out of reach. Choose raw pywin32 when you are porting VBA\nalmost line for line, when dependency count is genuinely constrained, or when the work lives\nentirely in corners of the object model the wrapper does not cover — and expect to meet the COM\nobject model either way.",[113,1476,1478],{"id":1477},"frequently-asked-questions","Frequently asked questions",[10,1480,1481,1485],{},[1482,1483,1484],"strong",{},"Is xlwings just a wrapper around pywin32?","\nOn Windows, largely yes — it uses pywin32 underneath and exposes the raw objects through the .api attribute. On macOS it wraps AppleScript instead, which is what makes the same script portable between the two.",[10,1487,1488,1491],{},[1482,1489,1490],{},"Does xlwings add measurable overhead?","\nA little per call, and it is irrelevant next to the cost of the COM crossing itself. Both are dominated by how many crossings your code makes, not by which library made them.",[10,1493,1494,1497],{},[1482,1495,1496],{},"Can I mix the two in one script?","\nYes, and it is the normal pattern. Use xlwings for the structure and drop to sheet.api or book.api for anything it has not wrapped — those are pywin32 objects and take pywin32 calls.",[10,1499,1500,1503],{},[1482,1501,1502],{},"Which is easier to install on a locked-down machine?","\npywin32 is a single package. xlwings adds itself plus pywin32, and its optional Excel add-in needs a separate step — though the add-in is only required for calling Python from Excel, not the other way round.",[113,1505,1507],{"id":1506},"related","Related",[1509,1510,1511,1518,1525,1532,1537],"ul",{},[1512,1513,1514,1515,1517],"li",{},"Up one level: ",[14,1516,17],{"href":16}," — the raw interface in detail.",[1512,1519,1520,1524],{},[14,1521,1523],{"href":1522},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002F","Automating Excel with xlwings Basics"," — the wrapper's own topic, from ranges to macros.",[1512,1526,1527,1531],{},[14,1528,1530],{"href":1529},"\u002Fgetting-started-with-python-excel-automation\u002Fchoosing-a-python-excel-library\u002Fwhen-to-use-xlwings-instead-of-openpyxl\u002F","When to Use xlwings Instead of openpyxl"," — the prior question of whether to drive Excel at all.",[1512,1533,1534,1536],{},[14,1535,812],{"href":811}," — the bug xlwings' context manager removes.",[1512,1538,1539,1543],{},[14,1540,1542],{"href":1541},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-xlwings-basics\u002Fxlwings-run-macro-from-python-example\u002F","xlwings Run Macro from Python Example"," — calling VBA through the wrapper.",[1545,1546,1547],"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-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 .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_b0D, html code.shiki .s_b0D{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#72F088;--shiki-dark-font-weight:bold}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}",{"title":123,"searchDepth":171,"depth":171,"links":1549},[1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562],{"id":115,"depth":171,"text":116},{"id":151,"depth":171,"text":152},{"id":724,"depth":171,"text":725},{"id":894,"depth":171,"text":895},{"id":980,"depth":171,"text":981},{"id":1099,"depth":171,"text":1100},{"id":1127,"depth":171,"text":1128},{"id":1240,"depth":171,"text":1241},{"id":1263,"depth":171,"text":1264},{"id":1373,"depth":171,"text":1374},{"id":1467,"depth":171,"text":1468},{"id":1477,"depth":171,"text":1478},{"id":1506,"depth":171,"text":1507},"2026-09-04","Both drive the same Excel through the same interface. Compare the code for one task, see what the wrapper adds, and learn when the raw object model is still the better choice.","md",[1567,1569,1571,1573],{"q":1484,"a":1568},"On Windows, largely yes — it uses pywin32 underneath and exposes the raw objects through the .api attribute. On macOS it wraps AppleScript instead, which is what makes the same script portable between the two.",{"q":1490,"a":1570},"A little per call, and it is irrelevant next to the cost of the COM crossing itself. Both are dominated by how many crossings your code makes, not by which library made them.",{"q":1496,"a":1572},"Yes, and it is the normal pattern. Use xlwings for the structure and drop to sheet.api or book.api for anything it has not wrapped — those are pywin32 objects and take pywin32 calls.",{"q":1502,"a":1574},"pywin32 is a single package. xlwings adds itself plus pywin32, and its optional Excel add-in needs a separate step — though the add-in is only required for calling Python from Excel, not the other way round.",{"breadcrumb":1576},[1577,1580,1583],{"name":1578,"item":1579},"Home","\u002F",{"name":1581,"item":1582},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":17,"item":16},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fxlwings-vs-pywin32-for-excel-automation",{"title":5,"description":1586},"xlwings wraps the COM interface pywin32 exposes: context managers, DataFrame conversion and range expansion versus Excel's documented object model and one dependency.","xlwings-vs-pywin32-for-excel-automation","getting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fxlwings-vs-pywin32-for-excel-automation\u002Findex","how-to","t0GdCUFYLbl3Ig5Pq-UG3k1UE913gPACSPdROp6SAng",[1592,1596],{"title":1593,"path":1594,"stem":1595,"children":-1},"Run an Excel Macro from Python with win32com","\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Frun-an-excel-macro-from-python-with-win32com","getting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Frun-an-excel-macro-from-python-with-win32com\u002Findex",{"title":1597,"path":1598,"stem":1599,"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",1788710154301]