xlwings: Run a VBA Macro From Python (Example)
To run a VBA macro from Python with xlwings, you open the workbook in a live Excel instance, get a callable handle to the macro by name, and invoke it. xlwings routes the call through Excel's COM bridge (Windows) or AppleScript (macOS), so the Sub executes exactly as it would from the VBA editor — including any sheet edits, pivot refreshes, or exports it performs.
There are two ways to get that handle: app.macro("Name") (resolved at the Excel-application level) and book.macro("Name") (resolved against a specific workbook). Both are current and supported; app.macro() was added in xlwings 0.24.0 for application-scoped resolution, while book.macro() has been around longer. This page shows a complete, reusable example and the details that trip people up.
Prerequisites
Because xlwings drives the desktop Excel application over its COM bridge (Windows) or AppleScript (macOS), the environment matters as much as the code. Before you run the example, confirm all of the following:
- A local Excel install with an interactive desktop. This runs on Windows (Excel 2010+) or macOS (Excel 2016+) with a logged-in graphical session. It will not run on headless Linux, bare Docker images, or CI services such as GitHub Actions or AWS Lambda. If your target is a server with no Excel, you cannot run VBA there — port the macro's logic to Python instead, using pandas for data work and openpyxl for file edits, both of which run headless.
- xlwings installed:
pip install xlwings. On Windows this pulls inpywin32; macOS uses AppleScript and needs no extra COM layer. - A macro-enabled workbook —
.xlsm,.xlsb, or.xlam— that actually contains theSubyou want to call. A plain.xlsxcannot store VBA, so there is nothing to run. - Macros enabled for that file. An untrusted macro-enabled workbook shows a security prompt before it will run any code, and that dialog will hang an unattended job. Put the file in a Trusted Location, or allow macros under File > Options > Trust Center > Macro Settings.
If you are new to the App/Book/Sheet/Range object model this example uses, the parent guide Automating Excel with xlwings Basics walks through those objects first.
Step-by-step solution
This function opens a workbook, binds a macro by name, calls it with any positional arguments you pass, saves, and shuts Excel down in a finally block so no process is left running:
import xlwings as xw
from pathlib import Path
def run_macro(workbook_path: str, macro_name: str, *args):
"""Open a macro-enabled workbook, run a VBA Sub, save, and clean up."""
app = xw.App(visible=False, add_book=False) # visible=True to debug dialogs
book = None
try:
book = app.books.open(str(Path(workbook_path).resolve()))
# Qualify the name with the workbook so resolution is unambiguous
macro = app.macro(f"'{book.name}'!{macro_name}")
macro(*args) # arguments are passed positionally
book.save()
finally:
if book is not None:
book.close()
app.quit()
if __name__ == "__main__":
run_macro(r"C:\reports\monthly_summary.xlsm", "FormatAndExport", True)
The matching VBA lives in a standard module in the workbook:
Sub FormatAndExport(ByVal exportPdf As Boolean)
' ... formatting / export logic ...
End Sub
Using book.macro() instead
If you are on an older xlwings, or simply prefer scoping to the workbook you already have open, book.macro() is equivalent. It doesn't need the 'Workbook'! qualifier because the workbook is implied:
book.macro(macro_name)(*args)
Both forms end up calling the same Sub; they differ only in where xlwings looks up the name and therefore whether you must spell out the workbook:
Passing arguments
xlwings forwards arguments to VBA positionally — there are no keyword arguments. Order matters, and the types map naturally: Python bool to VBA Boolean, int/float to numeric types, str to String.
# VBA: Sub BuildReport(ByVal region As String, ByVal year As Long)
run_macro(r"C:\reports\summary.xlsm", "BuildReport", "North", 2024)
For VBA Optional parameters you want to skip, pass None (it maps to VBA's missing/Empty) or an explicit default like "" or 0 to be unambiguous. Macros that take no arguments are just called with ():
book.macro("RefreshAll")()
Common pitfalls and gotchas
AttributeError: 'App' object has no attribute 'macro' — your xlwings predates 0.24.0. Either upgrade with pip install --upgrade xlwings, or switch to the book.macro() form, which works on older versions.
Macro not found / 'WorkbookName'!Name errors — Excel resolves the name strictly. Wrap workbook names that contain spaces in single quotes: app.macro("'My Report.xlsm'!Build"). For a macro in your personal macro workbook, qualify it with that file: app.macro("'PERSONAL.XLSB'!MyRoutine").
com_error / pywintypes.com_error (Windows) or an AppleScript timeout (macOS) — Excel is blocked, often by a modal dialog or a macro-security prompt. Set app.display_alerts = False before opening, verify the file isn't already open elsewhere, and terminate any orphaned process (taskkill /F /IM EXCEL.EXE on Windows) before retrying.
Excel process lingers after the script ends — an exception escaped before app.quit(). Keep the try/finally so book.close() and app.quit() always run, even on failure.
Headless / server run fails outright — expected: there is no Excel to drive. Move the logic to pandas/openpyxl as noted above, or run on a Windows workstation with Excel installed.
Performance and scale notes
The single most important number here is the cost of a cross-process call. Every macro(...) invocation, every .value read, and every property you touch is a COM round-trip into a separate Excel process, and those round-trips dominate wall-clock time far more than the VBA itself does. A few practical consequences:
- Do the heavy lifting inside VBA, not across the bridge. One macro call that loops over 50,000 rows in-process is dramatically faster than 50,000 individual reads or writes from Python. If you find yourself calling small macros in a tight Python loop, push that loop down into a single VBA
Sub. - Reuse one
Appfor a batch. Launching Excel costs a second or two. When you run macros across many workbooks, start onexw.App(visible=False, add_book=False), loop over the files opening and closing eachBook, and quit the app once at the end — rather than spawning a fresh Excel per file. - Keep Excel quiet during the run.
visible=Falseavoids screen repaints, and settingapp.display_alerts = Falseandapp.screen_updating = Falsebefore a long macro removes prompts and redraw overhead. Restore them (or just quit) afterwards. - Expect roughly linear scaling with workbook count, not data size. Because the work happens in native Excel, a macro that formats a large sheet is not much slower per row than a small one; the fixed per-file open/save/close overhead is usually what adds up across a batch.
For scheduled, unattended batches — the common production case — combine this with a real scheduler rather than leaving a script running. See running a Python Excel script on Windows Task Scheduler for the desktop-session and permissions details that trip up headless macro runs.
Where a macro belongs
Conclusion
Running a VBA macro from Python is a two-line operation once the setup is right: get a callable with app.macro("'Book'!Name") or book.macro("Name"), then call it with positional arguments. The parts that actually cause trouble are around that call — a live Excel install, a macro-enabled file, disabled security prompts, and a try/finally that always quits the app. Get those four right and the macro runs identically to clicking it in the VBA editor, while your Python code stays in control of the open–run–save–quit lifecycle.
Frequently asked questions
What's the difference between app.macro() and book.macro()?app.macro() resolves the name at the Excel-application level and needs a 'Workbook'!Name qualifier; book.macro() resolves against a specific workbook so the qualifier is implied. Both are current — app.macro() was added in xlwings 0.24.0, while book.macro() works on older versions too.
Can I pass keyword arguments to a VBA macro?
No. xlwings forwards arguments positionally only, so order matters. Types map naturally: Python bool to VBA Boolean, int/float to numeric types, and str to String.
How do I skip an Optional VBA parameter?
Pass None, which maps to VBA's missing/Empty, or pass an explicit default like "" or 0 to be unambiguous. A macro that takes no arguments is just called with ().
Why do I get AttributeError: 'App' object has no attribute 'macro'?
Your xlwings predates 0.24.0. Either upgrade with pip install --upgrade xlwings, or switch to the book.macro() form, which works on older releases.
Why does my unattended run hang on a macro-security prompt? An untrusted macro-enabled file prompts before enabling macros, and that dialog blocks a headless run. Put the workbook in a Trusted Location, or enable macros under File > Options > Trust Center > Macro Settings.
Related
- Up to the guide this belongs to: Automating Excel with xlwings Basics — the
App,Book,Sheet, andRangeobject model and the full open–write–run–save–quit lifecycle. - Writing DataFrames to Excel with Pandas — stage the data your macro formats before handing the workbook to xlwings for the VBA step.
- Run a Python Excel script on Windows Task Scheduler — the desktop-session and permission settings needed to run a macro job unattended.
- Schedule recurring Excel reports with APScheduler — drive the macro on a recurring cadence from a long-running Python process.
- Fill an Excel template with Python and openpyxl — a headless alternative when the "macro" is really just formatting a template.