Guide
Getting Started With Python Excel AutomationDeep dive

Run an Excel Macro from Python with win32com

Call existing VBA from Python with Application.Run: qualified macro names, positional arguments, return values, and staging bulk data so the loop runs inside Excel.

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.

How a macro call travels from Python into Excel The script opens the workbook that contains the macro, calls Application.Run with a qualified name, and Excel executes the VBA in its own process and returns the function's value. one call, one crossing open the .xlsm the macro must be loaded Application.Run qualified name plus arguments VBA runs in Excel returns a value to Python Python schedules and supplies inputs; VBA does the work in-process

Prerequisites

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

Text
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

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

The four naming rules behind most macro-not-found errors Qualify with the file name, quote names containing spaces, use a sheet's code name for macros in sheet modules, and make the macro public and its workbook open. 1 Qualify with the file name "automation.xlsm!BuildSummary", not the path 2 Quote names with spaces "'monthly report.xlsm'!BuildSummary" 3 Use the code name for sheet macros Sheet1.Recalculate, not the tab label 4 Public, and already open Run searches loaded workbooks only four rules cover nearly every failure of this call

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.

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

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

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

SymptomCauseFix
Cannot run the macro ... may not be availableThe workbook is not open, or the name is unqualifiedOpen the file first and qualify with "file.xlsm!Macro"
Works by hand, fails from PythonMacros disabled by Trust Center for this accountPut the file in a trusted location
A Private Sub is not foundPrivate members are not exposed to callersMake it Public, or add a public wrapper
The call returns None unexpectedlyIt is a Sub, not a FunctionChange to Function, or read the result from a cell
The script hangs with no outputThe macro raised and VBA is showing an error dialogSet DisplayAlerts = False and add On Error handling in the macro
xlsm saved as xlsx afterwards, macros goneThe extension decides whether VBA survivesSave as .xlsm, and see macro-enabled files in openpyxl

Performance and scale

Where the loop should live Driving twenty thousand cells from Python means twenty thousand process crossings, while passing a range address to the macro lets VBA do the same loop inside Excel with one crossing. loop in Python 20,000 COM calls minutes of runtime each cell crosses loop in VBA 1 COM call seconds of runtime no boundary inside pass a range give the macro an address, not a value at a time

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.

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