xlwings vs pywin32 for Excel Automation
xlwings and pywin32 reach the same Excel through the same interface, so the choice between them is about ergonomics rather than capability. That makes it an easy decision most of the time and a genuinely close one in a few cases. This guide, part of Automating Excel with COM and pywin32, compares them on the operations a reporting script actually performs.
Prerequisites
pip install xlwings pywin32
Windows with Excel installed. xlwings also runs on macOS; pywin32 does not.
The same job, written twice
Nothing shows the difference faster than one task in both libraries. Here is opening a workbook, reading a block, summarising it and writing the result back.
# pywin32
import win32com.client as win32
excel = win32.DispatchEx("Excel.Application")
excel.Visible = False
excel.DisplayAlerts = False
try:
book = excel.Workbooks.Open(r"C:\data\orders.xlsx")
try:
sheet = book.Sheets("Data")
last = sheet.Cells(sheet.Rows.Count, 1).End(-4162).Row
rows = sheet.Range(sheet.Cells(2, 1), sheet.Cells(last, 4)).Value
totals = {}
for row in rows:
totals[row[1]] = totals.get(row[1], 0) + row[3]
out = book.Sheets("Summary")
out.Range(out.Cells(2, 1), out.Cells(len(totals) + 1, 2)).Value = [
[k, v] for k, v in totals.items()
]
book.Save()
finally:
book.Close(SaveChanges=False)
finally:
excel.Quit()
# xlwings
import xlwings as xw
with xw.App(visible=False) as app:
book = app.books.open(r"C:\data\orders.xlsx")
frame = book.sheets["Data"].range("A1").options("df", expand="table").value
summary = frame.groupby("Region", as_index=False)["Revenue"].sum()
book.sheets["Summary"].range("A1").options(index=False).value = summary
book.save()
Six lines against twenty-five, and the six do not have a cleanup bug waiting in them. That gap — a context manager, DataFrame conversion and range expansion, all built in — is the whole argument for xlwings.
Where the wrapper actually helps
Four features account for most of the difference. xw.App is a context manager, so the application
quits even when the body raises — the failure mode covered in
Close Excel Cleanly and Avoid Orphan COM Processes.
options("df") converts a range to and from a DataFrame in one call rather than fifteen lines of
tuple juggling. expand="table" finds the extent of a block without the End(xlUp) incantation.
And collections are Pythonic: for sheet in book.sheets works, where the COM collection needs
either an index loop or an awkward iteration.
import xlwings as xw
book = xw.books.active
for sheet in book.sheets: # ordinary iteration
used = sheet.used_range
print(sheet.name, used.shape, used.address)
Where pywin32 still wins
Three situations favour the raw interface. Translating an existing macro is the strongest: the VBA you are porting names COM objects and methods directly, and mapping them onto xlwings' abstractions adds a translation step where matching them one for one does not.
Reaching an unwrapped corner of Excel is the second. xlwings covers what most scripts need, but
mail merge, some chart and shape properties, Application-level options and several print settings
are only available through the object model — and reaching them means sheet.api, which is a
pywin32 object anyway.
import xlwings as xw
sheet = xw.books.active.sheets["Summary"]
sheet.api.PageSetup.Orientation = 2 # xlLandscape, straight through the wrapper
sheet.api.PageSetup.Zoom = False
sheet.api.PageSetup.FitToPagesWide = 1
Dependency footprint is the third and least interesting: pywin32 is one package, xlwings is two. That matters on a locked-down machine where every dependency needs approval, and nowhere else.
The escape hatch, and why it matters
.api is the reason this is rarely an either/or decision. Every xlwings object exposes the
underlying COM object, so a script can use the friendly API for structure and drop to the raw model
for the one call that needs it.
import xlwings as xw
with xw.App(visible=False) as app:
book = app.books.open(r"C:\finance\model.xlsm")
book.macro("RebuildSummary")("2026-Q3") # xlwings
book.api.RefreshAll() # raw COM
app.api.CalculateUntilAsyncQueriesDone() # raw COM
book.save()
Learning the COM object model is therefore not wasted effort even if you use xlwings exclusively — it is what you fall back on the first time the wrapper does not reach far enough, and it is what the recorded VBA in front of you is written in.
Portability and platform
xlwings runs the same script on Windows and macOS by swapping COM for AppleScript underneath. That is genuinely useful for a tool a team runs on their own machines, and irrelevant for a scheduled job that will only ever run on one server. Neither library helps on Linux, where the answer remains the file-level libraries described in Choosing a Python Excel Library.
xlwings also has a second half that pywin32 has no equivalent of: running Python from Excel,
through user-defined functions and the RunPython macro. If the requirement includes a button in a
workbook, xlwings is not merely the friendlier choice but the only one —
Call Python from Excel with an xlwings UDF
covers that direction.
Migrating from one to the other
Because both libraries end up at the same object model, moving between them is mechanical rather
than a rewrite. Going from pywin32 to xlwings, the translation is largely about deleting: the
lifecycle boilerplate becomes a with block, the range sizing becomes expand="table", and the
tuple-of-tuples handling becomes an options call.
# pywin32
last = sheet.Cells(sheet.Rows.Count, 1).End(-4162).Row
rows = sheet.Range(sheet.Cells(2, 1), sheet.Cells(last, 4)).Value
# xlwings
rows = sheet.range("A2").expand("table").value
The other direction is just as mechanical, but the checklist is longer because the boilerplate has
to come back. Every with xw.App(...) becomes an explicit try/finally with a Quit, every
options("df") becomes a manual header-plus-body construction, and every collection iteration
becomes an index loop. It is worth doing only when one of the three reasons above genuinely applies,
because none of the reintroduced code adds capability.
A useful intermediate exists: keep xlwings for the session and write the operations against .api.
That gives the context manager and the platform handling while the calls themselves read like the
VBA they came from, which makes it a good landing point when porting a large macro incrementally.
Choosing for a team rather than a script
The last consideration is not technical. A script written with xlwings can be read by somebody who
knows Python and has never seen Excel's object model; a pywin32 script assumes familiarity with
Application, Workbooks, Range and the constants, and reads like VBA transliterated. On a team
where the Excel automation is maintained by whoever is available rather than by a specialist, that
difference in legibility outweighs almost everything else in this comparison.
The counterweight is that the object model is the shared vocabulary between Python and every macro already in the organisation. A team maintaining a large body of VBA has that knowledge anyway, and for them the raw interface is the one that matches what they already read. Deciding on that basis — who maintains this in a year — tends to produce a better answer than comparing feature lists, and it is the same reasoning behind the packaging choices in Testing and Packaging Excel Automation Scripts.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| An xlwings method does not exist | The feature is not wrapped | Use .api and call the COM member directly |
Constants like xlLandscape are undefined in xlwings | It does not re-export the type library | Use the integer, or win32com.client.constants |
| Scripts differ between Windows and macOS | Raw .api calls are platform-specific | Keep .api use behind a small platform check |
App context manager closes a user's Excel | It started a new instance but another was attached | Use xw.apps.active deliberately when acting on a live session |
| Mixed libraries fight over the same instance | Two applications started independently | Get one App and pass its .api around |
Performance and scale
Neither library changes the cost model: both pay per boundary crossing, and both are fast when ranges move in blocks. xlwings' DataFrame conversion is a single crossing plus a local conversion, so it is not slower than doing the same thing by hand — and it is frequently faster in practice, because the built-in path batches correctly where hand-written code often does not.
The one place raw pywin32 can win on performance is when a script needs Value2 semantics or an
unusual read shape that the wrapper converts unnecessarily. Reading a hundred thousand cells as raw
serials and converting them yourself avoids a per-cell datetime construction, which is measurable —
though it is a narrow enough case that it should be a response to a profile rather than a default.
Conclusion
Default to xlwings. It removes the cleanup bug, converts DataFrames for free, and keeps the script
readable, while .api means nothing is out of reach. Choose raw pywin32 when you are porting VBA
almost line for line, when dependency count is genuinely constrained, or when the work lives
entirely in corners of the object model the wrapper does not cover — and expect to meet the COM
object model either way.
Frequently asked questions
Is xlwings just a wrapper around pywin32? 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.
Does xlwings add measurable overhead? 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.
Can I mix the two in one script? 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.
Which is easier to install on a locked-down machine? 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.
Related
- Up one level: Automating Excel with COM and pywin32 — the raw interface in detail.
- Automating Excel with xlwings Basics — the wrapper's own topic, from ranges to macros.
- When to Use xlwings Instead of openpyxl — the prior question of whether to drive Excel at all.
- Close Excel Cleanly and Avoid Orphan COM Processes — the bug xlwings' context manager removes.
- xlwings Run Macro from Python Example — calling VBA through the wrapper.