Run an Excel Macro from Python with win32com
A great deal of working Excel automation already exists as VBA, and rewriting it in Python is
usually neither necessary nor wise. Application.Run calls an existing macro from a Python script
and hands back whatever it returns, which makes it possible to keep the macro as the unit of work
and let Python handle scheduling, inputs and delivery around it. This guide, part of
Automating Excel with COM and pywin32,
covers the call, its arguments, and the naming rules that account for most of its failures.
Prerequisites
pip install pywin32
Windows with Excel installed, and a workbook containing the macro. If macro security is set to disable everything, the file needs to be in a trusted location before any of this works.
Create a macro to call
Save this as a module in automation.xlsm — Developer, Visual Basic, Insert Module — so the
examples below have something real to call.
Public Function BuildSummary(quarter As String) As Long
Dim ws As Worksheet
Set ws = ThisWorkbook.Sheets("Summary")
ws.Range("A1").Value = "Summary for " & quarter
ws.Range("B2").Value = Application.WorksheetFunction.Sum(ThisWorkbook.Sheets("Data").Range("C:C"))
BuildSummary = ThisWorkbook.Sheets("Data").UsedRange.Rows.Count
End Function
It takes an argument, writes to the sheet and returns a number — enough to exercise every part of the call.
Run it from Python
import win32com.client as win32
excel = win32.DispatchEx("Excel.Application")
excel.Visible = False
excel.DisplayAlerts = False
try:
book = excel.Workbooks.Open(r"C:\automation\automation.xlsm")
try:
rows = excel.Application.Run("automation.xlsm!BuildSummary", "2026-Q3")
print(f"macro processed {rows} rows")
book.Save()
finally:
book.Close(SaveChanges=False)
finally:
excel.Quit()
The macro name is qualified with the file that holds it. Without that qualification Excel searches the active workbook, which is whichever one happens to be in front — a source of failures that only appear when a second workbook is open.
Naming rules that decide whether the call works
Four rules cover nearly every "macro cannot be found" error.
The workbook name goes before an exclamation mark, and it is the file name rather than a path:
"automation.xlsm!BuildSummary". If the name contains a space it must be wrapped in single quotes:
"'monthly report.xlsm'!BuildSummary".
A macro in a standard module can be called by its bare name. A macro inside a worksheet's code
module needs the sheet's code name — the one shown in the VBA project tree, not the tab label —
as in "automation.xlsm!Sheet1.Recalculate".
Private Sub is not callable from outside the project. Change it to Public, or add a public
wrapper that calls it.
And the workbook has to be open. Application.Run does not open files; it searches what is loaded.
Arguments, types and return values
Arguments are positional and are converted automatically: Python strings become VBA String,
integers become Long, floats become Double, and datetime objects become VBA dates. Up to
thirty arguments are allowed, which is far more than any sane macro signature.
result = excel.Application.Run(
"automation.xlsm!BuildRegional",
"North", # String
2026, # Long
0.175, # Double
True, # Boolean
)
A VBA Function returns its value; a Sub returns None. If a macro needs to hand back several
values, the workable options are to return a delimited string, or — usually better — to have the
macro write its results into a scratch range and read that range from Python afterwards, using the
block reads described in
Read and Write Cell Ranges with win32com.
Passing a lot of data into a macro
Do not pass a large array as an argument. The conversion is slow and the argument limit is low. Write the data into a staging sheet in one range assignment, then call the macro with the address:
staging = book.Sheets("Staging")
staging.Range("A1:C1").Value = [["Region", "Rep", "Revenue"]]
staging.Range(f"A2:C{len(rows) + 1}").Value = rows # one crossing
excel.Application.Run("automation.xlsm!ProcessStaging", f"A2:C{len(rows) + 1}")
This keeps the expensive part — moving values across the process boundary — to a single call, and leaves the macro doing what it is good at.
Deciding what stays in VBA
A script that can call macros invites a question that is worth answering deliberately: which half of the logic belongs where. The useful test is whether the work depends on Excel. Anything that reads a database, formats an email, decides which files to process or writes a log belongs in Python, where it can be tested, version-controlled and run on a schedule. Anything that manipulates the workbook in bulk — filling a thousand rows, applying a template's formatting, rebuilding a chart — is faster inside VBA, because it happens in the same process as the data.
That division also survives change better than either extreme. A macro that has grown to include database credentials and file paths is a macro that nobody can move; a Python script that drives Excel cell by cell is one that nobody can speed up. Keeping the boundary at "Python decides, VBA manipulates" leaves each side doing what it is good at, and makes the eventual migration away from VBA a matter of replacing one call rather than untangling a program.
There is a practical corollary. Macros called from Python should take arguments rather than reading their own configuration. A macro that opens a hard-coded path is one that can only ever do one job; the same macro taking a range address and a multiplier can be called for any month, any region and any file the script chooses.
Handling a macro that fails
VBA errors do not arrive as Python exceptions in a useful form. An unhandled error inside a macro
raises a dialog, and with DisplayAlerts suppressed the call typically comes back as a generic COM
error naming the Run method rather than the problem. The workable pattern is to have the macro
catch its own errors and report them as a return value.
Public Function BuildSummary(quarter As String) As String
On Error GoTo Failed
' ... work ...
BuildSummary = "OK"
Exit Function
Failed:
BuildSummary = "ERROR: " & Err.Number & " " & Err.Description
End Function
outcome = excel.Application.Run("automation.xlsm!BuildSummary", "2026-Q3")
if not str(outcome).startswith("OK"):
raise RuntimeError(f"macro failed: {outcome}")
Twelve extra lines turn an opaque hang into a message that names the failing line's error number, which is the difference between fixing it in five minutes and reproducing it by hand on a Windows machine. The same reasoning applies to the retry and logging patterns in Retry a Failed Excel Report Job in Python.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
Cannot run the macro ... may not be available | The workbook is not open, or the name is unqualified | Open the file first and qualify with "file.xlsm!Macro" |
| Works by hand, fails from Python | Macros disabled by Trust Center for this account | Put the file in a trusted location |
A Private Sub is not found | Private members are not exposed to callers | Make it Public, or add a public wrapper |
The call returns None unexpectedly | It is a Sub, not a Function | Change to Function, or read the result from a cell |
| The script hangs with no output | The macro raised and VBA is showing an error dialog | Set DisplayAlerts = False and add On Error handling in the macro |
xlsm saved as xlsx afterwards, macros gone | The extension decides whether VBA survives | Save as .xlsm, and see macro-enabled files in openpyxl |
Performance and scale
The macro itself runs at VBA speed, which is usually fine — VBA inside Excel is far faster than COM calls from outside it, because there is no process boundary to cross. That produces a useful rule: work that touches many cells belongs inside the macro, and Python should hand it a range address rather than iterating cells itself.
# Slow: Python drives every cell through COM.
for index in range(2, 20002):
sheet.Cells(index, 4).Value = sheet.Cells(index, 2).Value * 1.2
# Fast: one call, and VBA does the loop in-process.
excel.Application.Run("automation.xlsm!ApplyUplift", "B2:B20001", 1.2)
Two further settings help when a macro rewrites a lot of the sheet: turning off screen updating and switching calculation to manual for the duration. Both are set from Python before the call and restored afterwards, and together they routinely halve the runtime of a heavy macro.
Conclusion
Application.Run lets an existing VBA investment keep earning while Python takes over the parts it
is better at — inputs, scheduling, delivery and logging. Qualify the macro name with its workbook,
make sure that workbook is open, keep the macro Public, and push bulk work into VBA rather than
looping over cells from outside. When the macro needs a lot of data, stage it in a range with a
single assignment and pass the address.
Frequently asked questions
Why does Application.Run say the macro cannot be found? Almost always a naming problem. Qualify the macro with the workbook that contains it — "book.xlsm!ModuleName.MacroName" — and remember that a macro inside a sheet's code module needs the sheet's code name, not its tab name. A macro declared Private is not callable from outside at all.
Can I pass arguments and get a return value? Yes. Positional arguments follow the macro name in Application.Run, and a VBA Function returns its value straight back to Python. A Sub returns None because it has nothing to return.
Does the macro have to live in the workbook I opened? No. It can live in another open workbook, in an add-in, or in PERSONAL.XLSB. Qualify the name with whichever file holds it, and make sure that file is open before the call.
Why does the call fail with macros disabled? Trust Center settings block VBA in files from untrusted locations. Put the workbook in a trusted location, or enable macros for the account that runs the script — there is no COM-side override.
Related
- Up one level: Automating Excel with COM and pywin32 — the session skeleton these calls sit inside.
- xlwings Run Macro from Python Example — the same job through the friendlier wrapper.
- Read and Write Cell Ranges with win32com — staging data for a macro in one call.
- Handle COM Errors and Excel Dialog Prompts in Python — what to do when a macro raises inside Excel.
- Work with Macro-Enabled .xlsm Files in openpyxl — editing the same files without running them.