Guide
Getting Started With Python Excel AutomationDeep dive

xlwings vs pywin32 for Excel Automation

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.

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.

Both libraries reach the same Excel 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. your script xlwings or pywin32 xlwings optional convenience layer pywin32 / COM the actual interface EXCEL.EXE identical either way the capability is the same; the ergonomics are not

Prerequisites

Bash
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.

Python
# 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()
Python
# 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

What each library gives you for the same task 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. xlwings App is a context manager DataFrames both ways expand='table' iterable collections pywin32 the documented model matches recorded VBA one dependency manual lifecycle six lines against twenty-five, for the same result

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.

Python
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.

Python
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.

Python
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.

Python
# 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

SymptomCauseFix
An xlwings method does not existThe feature is not wrappedUse .api and call the COM member directly
Constants like xlLandscape are undefined in xlwingsIt does not re-export the type libraryUse the integer, or win32com.client.constants
Scripts differ between Windows and macOSRaw .api calls are platform-specificKeep .api use behind a small platform check
App context manager closes a user's ExcelIt started a new instance but another was attachedUse xw.apps.active deliberately when acting on a live session
Mixed libraries fight over the same instanceTwo applications started independentlyGet one App and pass its .api around

Performance and scale

Neither wrapper changes the cost model 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. per-cell loop, pywin32 50,000 crossings per-cell loop, xlwings 50,000 crossings block read, either 1 crossing relative cost the library is not the variable — the crossings are

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.