Run Python from an Excel Button with xlwings RunPython
Some automation belongs inside the workbook people already use: a Refresh button on a planning sheet that pulls the latest figures, a Validate button that checks what someone typed, a Build report button that turns the current selection into a formatted output. VBA can do the clicking; Python is better at the work behind it. xlwings joins the two: a one-line VBA macro calls RunPython, Python runs your function, and the function reads and writes the workbook that pressed the button. This guide, part of Automating Excel With xlwings: The Basics, builds that setup end to end.
Prerequisites
Desktop Excel on Windows or macOS, Python with xlwings, and the xlwings Excel add-in (unless you use a standalone workbook, Step 6):
pip install xlwings pandas
xlwings addin install
The add-in adds an xlwings tab to Excel's ribbon and provides the RunPython function to VBA. Macros must be enabled for the workbook, because the button runs a VBA macro.
Step 1 — Create a project with quickstart
xlwings can generate a matching workbook and Python file:
xlwings quickstart report
This creates a folder report containing report.xlsm and report.py. The workbook's VBA module already has a macro wired to the Python file, and by default xlwings looks for the Python module next to the workbook, so the two travel together. Keeping each button-driven tool in its own quickstart folder avoids the most common problem — Python not finding the module — before it starts.
Step 2 — Write the Python function
The Python side is an ordinary function. The only special line is xw.Book.caller(), which returns the workbook whose button started the run:
# report.py
import pandas as pd
import xlwings as xw
def main():
wb = xw.Book.caller()
data = wb.sheets["Data"].range("A1").options(pd.DataFrame, header=1, index=False,
expand="table").value
summary = (data.groupby("region", as_index=False)["amount"].sum()
.sort_values("amount", ascending=False))
out = wb.sheets["Summary"]
out.clear_contents()
out.range("A1").options(index=False).value = summary
out.range("D1").value = f"Built {pd.Timestamp.now():%Y-%m-%d %H:%M} from {len(data)} rows"
The function reads the whole data block in one call and writes the summary in one call — the bulk pattern from Speed Up xlwings with Bulk Reads and Writes. A button that makes the user wait is a button they stop using.
Step 3 — Wire the button
In the workbook's VBA editor (Alt+F11), the macro that calls Python is one line:
Sub BuildReport()
RunPython "import report; report.main()"
End Sub
Then add the button: on the Developer tab choose Insert → Button (Form Control), draw it on the sheet, and assign the BuildReport macro. The string passed to RunPython is Python code, so it can import any module the interpreter can find and call any function in it. Keep it to an import and a call — logic in that string is hard to read and impossible to test.
Step 4 — Pass arguments from the sheet
Different buttons often run the same function with different settings — one per region, say. Either pass literal arguments in the RunPython string, or better, have the function read its inputs from named cells so users change them without touching VBA:
def main_for_region():
wb = xw.Book.caller()
region = wb.sheets["Controls"].range("SelectedRegion").value
start = wb.sheets["Controls"].range("StartDate").value # arrives as a datetime
data = wb.sheets["Data"].range("A1").options(pd.DataFrame, header=1, index=False,
expand="table").value
view = data[(data["region"] == region) & (data["order_date"] >= start)]
wb.sheets["Summary"].range("A1").options(index=False).value = view
Named cells (SelectedRegion, StartDate) keep working when someone inserts rows above them, and a data-validation dropdown on the region cell stops typos from reaching the code. Dropdowns are covered in Add Dropdown Data Validation to Excel with openpyxl.
Step 5 — Configure the interpreter
RunPython needs to know which Python to start. Set it once in the xlwings ribbon (the Interpreter box) or, to keep the setting with the workbook, in a sheet named xlwings.conf with two columns of keys and values:
| Key | Value |
|---|---|
| Interpreter_Win | C:\tools\report-env\Scripts\python.exe |
| Interpreter_Mac | /Users/shared/report-env/bin/python |
| PYTHONPATH | C:\tools\report |
Pointing at a dedicated virtual environment keeps the tool's dependencies separate from anything else on the machine. If users have different paths, the ribbon setting per user is simpler; if the machine is shared or managed, the sheet travels with the workbook.
Step 6 — Test without clicking, and distribute
Clicking a button is a slow way to debug. set_mock_caller() makes a workbook opened from Python look like the caller, so the function runs from your editor, breakpoints and all:
if __name__ == "__main__":
xw.Book("report.xlsm").set_mock_caller()
main()
To give the tool to people who do not have the add-in, create it with xlwings quickstart report --standalone. That copies the xlwings VBA code into the workbook, so only Python and the xlwings package are needed on the user's machine. Macros must still be allowed: files that arrive by email or download are blocked by default, so distribute through a trusted network location or have users unblock the file.
Handling errors
When the Python function raises, xlwings shows the traceback in a message box — useful to you, alarming to users. Catch errors at the top of the function, show a plain message in the sheet, and keep the full traceback in a log file you can ask for:
import logging
import traceback
logging.basicConfig(filename="report.log", level=logging.INFO)
def safe_main():
wb = xw.Book.caller()
status = wb.sheets["Controls"].range("Status")
try:
main()
status.value = "Done"
except Exception:
logging.error(traceback.format_exc())
status.value = "Something went wrong — the details are in report.log"
Point the button at safe_main instead of main. For turning individual functions into Excel formulas rather than button actions, see Call Python from Excel with an xlwings UDF.
Button, formula or scheduled job?
A button is one of three ways to put Python behind a workbook, and it is worth choosing deliberately. A button suits an action a person decides to take at a moment of their choosing — rebuild this summary now, validate what I just typed, export this selection. A user-defined function suits a calculation that should update like any other formula whenever its inputs change. A scheduled job suits work that must happen whether or not anyone opens the workbook, such as producing the Monday report at 06:00.
Mixing them is common: a scheduled job refreshes the data sheet every morning, and a button lets a planner rebuild the summary after changing assumptions. Keep the shared logic in one Python module that both call, so the numbers agree however they were produced.
Common pitfalls
- Module not found. The Python file must be on the interpreter's path — next to the workbook by default, or in
PYTHONPATH. Most "No module named report" errors are a moved file. - Wrong interpreter. The ribbon or
xlwings.confpoints at a Python without xlwings or pandas installed. Check the interpreter path first when a button that works for you fails for a colleague. - Long runs with no feedback. Excel looks frozen while Python works. Write a "Working…" status before the heavy step and keep the work fast.
- Two users, one workbook. Each click runs Python on the clicking user's machine against the copy of the workbook they have open. If the workbook lives on a shared drive, two people pressing the button at once each change their own copy — co-authoring rules still apply to what gets saved.
- Editing the macro string. Logic inside
RunPython "..."is invisible to tests and linters. Keep it to an import and one call.
Conclusion
A button-driven Python tool has three parts: a button, a one-line VBA macro calling RunPython, and a Python function that finds its workbook with xw.Book.caller(). Generate the pair with xlwings quickstart, read inputs from named cells, configure the interpreter per user or in xlwings.conf, debug with set_mock_caller(), distribute with --standalone, and wrap the entry point so users see a plain message while the traceback goes to a log.
Frequently asked questions
What does RunPython do? RunPython is a VBA function provided by xlwings. It starts Python, runs the code string you give it — typically an import and a function call — and lets that code reach the workbook that called it through xw.Book.caller().
Do users need Python installed? Yes. RunPython runs Python on the user's machine, so each user needs a Python installation with xlwings and your code's dependencies, and the workbook's configuration must point at that interpreter.
How do I test the Python side without clicking the button? Open the workbook from Python and call set_mock_caller() on it. xw.Book.caller() then returns that workbook, so you can run and debug the function from your editor.
Can I distribute the workbook without the xlwings add-in? Yes. Create the project with xlwings quickstart --standalone, which copies the xlwings VBA module into the workbook itself, so recipients do not need the add-in — they still need Python and the xlwings package.
Related
- Up one level: Automating Excel With xlwings: The Basics — xlwings fundamentals.
- xlwings: Run a VBA Macro From Python — the opposite direction.
- Python vs VBA for Excel Automation — when a button tool is the right design.
- Keep Excel Report Settings in a Config File — settings outside the workbook.
More on xlwings
- How-toTest win32com and xlwings Code Without Excel
- How-toAutomate Excel on macOS with Python
- Comparisonxlwings vs pywin32 for Excel Automation
- How-toControl How xlwings Converts DataFrames and Ranges
- How-toFormat Cells in a Live Workbook with xlwings
- How-toInsert a matplotlib Plot into a Live Workbook with xlwings