# index.md
# xlwings Lite
**xlwings Lite lets you automate Excel and build native custom functions in Python.
Free, cross-platform, and without installing Python.**
 [Microsoft Marketplace](https://marketplace.microsoft.com/en-us/product/WA200008175?tab=Reviews) · **★★★★★** **5.0** · 43 ratings

> “I have used lots of ways to have user functions in Excel over the years: VBA, C and C++ XLLs and DLLs. […] xlwings Lite has basically fulfilled all my wishes. It’s like having the flexibility and ease of VBA but in a language with so much more potential. And this app is free!”
> — **Antoine Marmoiton**, [Microsoft Marketplace review](https://marketplace.microsoft.com/en-us/product/WA200008175?tab=Reviews)
[Install from the Office add-in store](https://appsource.microsoft.com/en-us/product/office/WA200008175). It’s free for personal and commercial use, with no registration or login required. See [Quick Start](quickstart.md) for step-by-step instructions and a video walkthrough.
After installation, `main.py` already contains sample code, so you can get started right away:
### Custom function
```python
@func
def hello(name):
return f"Hello {name}!"
```
Type `=HELLO("World")` into a cell and you’ll get back `Hello World!`—a native Excel custom function, written in Python.
### Script
```python
@script
def hello_world(book: xw.Book):
book.sheets.active["A1"].value = "Hello World!"
```
Click on the green **Run Hello World** button (or hit `F5`).
## Why xlwings Lite
- **No Python installation required**: Python runs via [Pyodide](https://pyodide.org) inside the browser runtime used by modern Office add-ins.
- **[Cross-platform](quickstart.md#prerequisites)**: Works on Windows, macOS, and Excel for the web. It’s compatible with Microsoft 365 as well as perpetual versions of Office from 2021 onward.
- **No service quotas**: There are no execution quotas, throttling, or per-user usage limits.
- **[Built on a proven foundation](about.md)**: xlwings Lite is based on xlwings, the open-source package that has been around since 2014.
## What you can do
- **[Custom functions](custom-functions.md)**: Define native user-defined functions (UDFs) that you call directly from a cell.
- **[Scripts](custom-scripts.md)**: Automate Excel (e.g., insert a sheet or color a cell) by running Python scripts at the click of a button.
- **[Notebooks](notebooks.md)**: As in Jupyter notebooks, write and run code cell by cell, with support for modern plotting libraries such as Matplotlib, seaborn, Plotly, Bokeh, and Altair.
- **[Install packages](dependencies.md)**: Use libraries like pandas, Polars, DuckDB, or your own Python packages.
- **[Web API access](web-requests.md)**: Fetch data directly from APIs.
- **[App Mode](app-mode.md)**: Turn a workbook into an app for end users. The code editor is hidden and each script becomes a button with auto-generated form fields.
## Developer experience
- **[VS Code editor](editor.md)**: xlwings Lite uses the same editor that powers VS Code, including many of its familiar keyboard shortcuts.
- **[Testing with pytest](tests.md)**: A built-in pytest runner lets you write automated tests for your Python code as well as for the workbook itself.
- **[Personal modules](personal-modules.md)**: Share Python files and packages across all your workbooks, similar to VBA’s `Personal.xlsb`. You can also track them with Git.
- **Wingman AI assistant**: Write and explain code with a built-in AI chat, plus the `=WINGMAN()` custom function. Bring your own API key for Claude, Gemini, OpenAI, or a self-hosted model.
- **Standard Python files**: Organize your code using regular `.py` modules, functions, and imports.
- **A path beyond WebAssembly**: If you outgrow the browser’s [Limitations](limitations.md), you can reuse the core xlwings API with the [local version of xlwings](https://docs.xlwings.org) or [xlwings Server](https://server.xlwings.org), usually with little or no code changes.
## Privacy, security & distribution
- **[Local-first](security.md)**: Python runs on your own computer.
- **Code you control**: Workbook code runs only after you approve it, with a view-only option for inspecting untrusted workbooks.
- **Easy distribution**: Python code is stored inside the workbook, making the xlwings Lite add-in the only requirement to run your workbook.
- **[Self-hosting](self-hosting.md)**: For maximum control, deploy xlwings Lite on your own infrastructure behind your firewall.
# about.md
# About
xlwings Lite is built on the strong foundation of xlwings, the open-source Python package. Established in 2014, xlwings has reached 20 million downloads, according to [pepy.tech](https://pepy.tech/projects/xlwings).
## Vision
xlwings Lite brings the VBA experience to the modern age with a focus on developer experience, privacy, and security.
## About me
Hi 👋, I’m Felix Zumstein, the creator of xlwings and xlwings Lite. I’m an independent software developer based in Lucerne. My company is called Zoomer Analytics GmbH.
User feedback is my only driving force, rather than catering to the growth fantasies of venture capitalists or executives with funny ideas.
I believe that Python should be easily accessible within Excel, which is why I made xlwings Lite available for free for both personal and commercial use in Excel’s add-in store. xlwings Lite works on Windows, macOS, and Excel on the web. It is compatible with Microsoft 365 as well as recent perpetual versions of Office, such as Office 2024.
## License & Privacy
For the xlwings Lite add-in as provided via Excel’s add-in store, you’ll find the EULA and privacy statements under these links:
- [EULA](https://www.xlwings.org/lite/eula)
- [Privacy Statement](https://www.xlwings.org/lite/privacy)
# app-mode.md
# App Mode
App Mode turns the xlwings Lite task pane into an app for end-users by hiding the code editor and showing one button for each script, rendering docstrings, and showing the available custom functions.
The following screenshot shows how the default code samples look in app mode (scripts tab on the left, custom functions tab on the right):

## Toggling between Developer Mode and App Mode
You can toggle between Developer Mode and App Mode via the Ctrl+Shift+M (Windows) or Cmd+Shift+M (macOS) keyboard shortcuts. Alternatively, go to **Settings > Workbook > App Mode** and click **Activate App Mode**. The setting is saved in the workbook, so it stays in App Mode the next time the workbook is opened.
## Form fields
If a script accepts arguments in addition to `book: xw.Book`, App Mode renders a form element for each one above the script’s button. The type hint decides which element is used — a text box for `str`, a number box for `int`/`float`, a checkbox for `bool`, a dropdown for `Literal[...]`, a date picker for `datetime.date`, and a date and time picker for `datetime.datetime`. Arguments without a type hint get a text box and are passed as strings. See [Script arguments](custom-scripts.md#script-arguments) for the full table and an example.

Fields are labeled with the parameter name unless the script gives them a friendlier label via `typing.Annotated`, which can also add help text above the field — see [Labels and help text](custom-scripts.md#labels-and-help-text).

Arguments without a default value are marked with a red asterisk and must be filled in before the script runs; if one is missing, App Mode shows an error message instead of running the script. Arguments that have a default are prefilled in the form element.
## Showing the Output pane
By default, App Mode hides the Output pane. If your scripts print progress or results that end-users should see, go to **Settings** > **Workbook** > **App Mode** and enable **Show the Output pane**. Like the mode itself, this setting is saved in the workbook, so it travels with the file.
## How does it work?
Module, `@script`, and `@func` docstrings are rendered as Markdown descriptions in App Mode.
This is how `main.py` looks that produces the above screenshot:
```python
"""
# xlwings Lite Demo
Select a script in the dropdown
above and click the button or press
F5 to run it.
To toggle between Developer and App
mode, hit `Ctrl+Shift+M` or
`Cmd+Shift+M` (macOS).
To start with an empty file, go to
Settings > Local > Editor and disable
'Load sample code for new workbooks'.
"""
import datetime as dt
import numpy as np
import pandas as pd
import seaborn as sns
import xlwings as xw
from xlwings import arg, func, script
@script(name="Run Hello World")
def hello_world(book: xw.Book):
"""# Hello World
The classic Hello World sample"""
...
@script(name="Run Seaborn Demo")
def seaborn_sample(book: xw.Book):
"""# Seaborn Sample
This sample showcases pandas, Seaborn, and Web APIs
"""
...
@script(name="Insert Custom Functions")
def insert_custom_functions(book: xw.Book):
"""# Custom Functions
This inserts a new sheet with a few custom functions
"""
...
@func
def hello(name: str):
"""The classic Hello World function"""
return f"Hello {name}!"
@func
@arg("rows", doc="Number of rows")
@arg("cols", doc="Number of columns")
def standard_normal(rows, cols):
"""Returns an array of standard normally distributed pseudo random numbers"""
...
@func
def correl2(df: pd.DataFrame):
"""Like CORREL, but it works on whole matrices instead of just 2 arrays.
The type hint converts the values of the range into a pandas DataFrame.
"""
...
```
## Trust prompt in App Mode
When a workbook in App Mode is opened, the trust prompt shows only a single **Trust and Continue** button.
A power user who wants to inspect the code before trusting can press `Ctrl+Shift+M` / `Cmd+Shift+M` on the trust prompt to reveal the **Don’t Trust (View Only)** option, which opens a read-only editor without running any Python code.
# async-api.md
# Async API
To interact with the Excel object model, scripts and notebooks let you use the same syntax as the classic, locally installed xlwings (*sync API*). This often works fine, but because xlwings Lite runs in a browser, it talks to Excel differently. That difference creates three limitations, and the *async API* gives you a method to work around each one:
1. **All cell values are loaded up front.** Getting a sync book (`xw.books.active` for notebooks or using the `xw.Book` type hint in scripts) fetches all values from the entire workbook, which can be slow and may cause the browser to run out of memory for large workbooks.
→ [`await myrange.get_value()`]() with an async `Book` reads only the values you ask for.
2. **Writes are queued.** When you assign a value to a range, for example, it’s only applied at the end of a notebook cell or when a script finishes, not immediately.
→ [`await book.flush()`]() applies queued writes immediately.
3. **Reads can be outdated.** Anything you read via the sync API reflects the workbook state from the start of the notebook cell or the start of the script.
→ [`await book.load()`]() refreshes everything.
The following sections cover each method in detail.
## `get_value()`: reading values on demand
To read values on demand:
- Use an async `Book` object, which disables the loading of all values up front:
- Notebook: `await xw.books.get_active()`
- Script: `xw.BookAsync` (type hint)
- Use `await myrange.get_value()` instead of `myrange.value` to load the values live.
In the following example, only the value of a single cell is loaded, no matter how big the workbook is:
### Notebook
```python
# %%
import xlwings as xw
book = await xw.books.get_active() # sync version: book = xw.books.active
myrange = book.sheets[0]["A1"]
await myrange.get_value() # sync version: myrange.value
```
### Script
```python
import xlwings as xw
from xlwings import script
@script
async def my_script(book: xw.BookAsync): # sync version: def my_script(book: xw.Book)
myrange = book.sheets[0]["A1"]
await myrange.get_value() # sync version: myrange.value
```
#### NOTE
- Notebooks let you `await` at the top level without requiring an `async def`.
- Scripts must use `await` inside of an `async def`.
`get_value()` respects the same converters and options as `value`, so you can, for example, read a range directly into a pandas DataFrame:
```python
import pandas as pd
df = await (
book.sheets["Sheet1"]["A1:C10"]
.options(pd.DataFrame, index=False)
.get_value()
)
```
While `await get_value()` technically also works with a sync book to get the current values, a sync book still loads the whole workbook up front.
## `flush()`: writing immediately
When writing to a workbook, xlwings Lite queues every action and applies them in order at the end of a notebook cell or when the script finishes. If you need to flush queued writes to Excel *before* the automatic flush happens, call `await book.flush()`:
### Notebook
```python
# %%
book = await xw.books.get_active()
book.sheets[0]["A1"].value = "Hello xlwings!"
await book.flush()
await book.sheets[0]["A1"].get_value()
# Output
# 'Hello xlwings!'
```
### Script
```python
import xlwings as xw
from xlwings import script
@script
async def my_script(book: xw.BookAsync):
book.sheets[0]["A1"].value = "Hello xlwings!"
await book.flush()
print(await book.sheets[0]["A1"].get_value())
# prints 'Hello xlwings!'
```
Here are some common scenarios where you need to use `await book.flush()`:
- After writing a value to an Excel cell, call `await book.flush()` before reading that value—or a dependent cell—within the same notebook cell or script (example above).
- If you want to see the printed output immediately in the Output pane, use `await book.flush()` after `print()`.
- If you call a method that writes to the file system, such as `mysheet["A1:D10"].to_png("/data/range.png")`, call `await book.flush()` before accessing the file in the same notebook cell or script.
## `load()`: reading the current workbook state
Before running a script or a notebook cell, xlwings Lite automatically runs `await book.load()`, so you rarely need to call it explicitly. To refresh the workbook state midway through a notebook cell or script, use `await book.load()` or `await mysheet.load()`:
### Notebook
```python
# %%
book = await xw.books.get_active()
sheet1 = book.sheets[0]
await book.load() # or: await sheet1.load()
sheet1.tables
```
### Script
```python
import xlwings as xw
from xlwings import script
@script
async def my_script(book: xw.BookAsync):
sheet1 = book.sheets[0]
await book.load() # or: await sheet1.load()
sheet1.tables
```
A few things to keep in mind:
- On an async book, `await book.load()` fetches everything *except* values since you should access values via `await myrange.get_value()`. On a sync book, `await book.load()` *includes* values. If you want to exclude values on a sync book, use `await book.load(values=False)`.
- Calling `load()` on a sheet object rather than the book loads only that sheet instead of the whole book, so it’s slightly more efficient.
# aws-ecs-express.md
# AWS ECS Express Mode
## 1. Create an ECS Express Mode Service
1. Go to the [ECS console](https://console.aws.amazon.com/ecs/v2) and select **Express mode** in the navigation pane on the left-hand side
2. **Image URI**:
```text
public.ecr.aws/xlwings/xlwings-lite:1.0.0.104
```
3. **Task execution role**: Select an existing role or choose `Create new role`
4. **Infrastructure role**: Select an existing role or choose `Create new role`
5. Expand the **Additional configurations** section:
- **Container port**: `8000`
- **Environment variables** > **Add environment variable**:
- **Key**: `XLWINGS_LICENSE_KEY`
- **Value type**: `Environment variable`
- **Value or value from**: ``
- **CPU**: 0.25 vCPU, **Memory**: 0.5 GB
- **Networking**: Configure according to your requirements (leaving the checkbox unchecked will expose the service to the public internet)
6. Click **Create**
## 2. Configuration (optional)
You can configure xlwings Lite via optional environment variables. For an overview, see [Configuration](configuration.md).
To add environment variables to your existing service, click on your service > Update service (orange button on the top right) > Additional configurations. Once added, click on Update (orange button at the bottom right) to deploy.
## 3. Add-in installation
1. In your browser, go to `https:///manifest`, which will download `xlwings-lite-manifest.xml`.
2. Go to [Microsoft 365 admin center](https://admin.microsoft.com/)
- Click on [`Show all` > `Settings` > `Integrated Apps`](https://admin.microsoft.com/#/Settings/IntegratedApps).
- If you have **xlwings Lite** installed, uninstall it first.
- Click on `Upload custom apps` and select `Office Add-in` (App type).
- Select `Upload manifest file (.xml) from device`. Click `Choose File`, then select the `xlwings-lite-manifest.xml` from the previous step.
- Click `Next`, then assign the desired users.
- Click `Next` and accept permission requests.
- Click `Next` and `Finish deployment`.
The users will get the add-in to show up automatically although it may take a few hours.
## 4. Updating
To update xlwings Lite:
1. Go to the [ECS console](https://console.aws.amazon.com/ecs/v2) and navigate to your cluster > your service
2. Click on Update service (orange button on the top right). Update the **Image URI** to the following:
```text
public.ecr.aws/xlwings/xlwings-lite:1.0.0.104
```
Once added, click on Update (orange button at the bottom right) to deploy.
3. Normally, you don’t need to update the manifest after deploying a new version of the container. Occasionally, this is required if e.g., if the URL of your container changed. In this case, the Office 365 admin center offers a link to **Update Add-in**.
# azure-container-apps-cli.md
# Azure Container Apps (Azure CLI)
#### NOTE
These Azure CLI commands are provided as a convenience and have not been fully verified end-to-end. The [Azure Container Apps](azure-container-apps.md) guide (using the Azure portal) is the supported path. Treat the commands below as a starting point and double-check them against the current Azure CLI documentation.
## Deploying the container
### Prerequisites
- [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) installed and logged in (`az login`)
- An xlwings [license key](https://www.xlwings.org/trial)
### Setup environment variables
Note that some of these values *have* to be changed, as they have to be globally unique.
```bash
export RESOURCE_GROUP_NAME="xlwings-lite-rg"
export APP_NAME="xlwings-lite"
export ENVIRONMENT_NAME="xlwings-lite-env"
export LOCATION="westeurope"
export XLWINGS_LITE_VERSION="1.0.0.104"
export XLWINGS_LICENSE_KEY="your-license-key-here"
```
### Deployment steps
1. Create Resource Group if you don’t want to reuse an existing one
```bash
az group create \
--name $RESOURCE_GROUP_NAME \
--location $LOCATION
```
2. Create Container Apps Environment if you don’t want to reuse an existing one (you may need to specify more options if you want to expose this on an virtual network)
```bash
az containerapp env create \
--name $ENVIRONMENT_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--location $LOCATION
```
3. Create Container App
```bash
az containerapp create \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--image docker.io/xlwings/xlwings-lite:$XLWINGS_LITE_VERSION \
--environment $ENVIRONMENT_NAME \
--min-replicas 1 \
--max-replicas 1 \
--cpu 0.5 \
--memory 1.0Gi \
--target-port 8000 \
--ingress external
```
To change the replica count on an existing app:
```bash
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--min-replicas 1 \
--max-replicas 1
```
4. Update the Container App with environment variables
```bash
FQDN=$(az containerapp show --name $APP_NAME --resource-group $RESOURCE_GROUP_NAME --query properties.configuration.ingress.fqdn -o tsv)
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--image xlwings/xlwings-lite:$XLWINGS_LITE_VERSION \
--set-env-vars XLWINGS_LICENSE_KEY=$XLWINGS_LICENSE_KEY XLWINGS_HOSTNAME=$FQDN
```
### View application logs
```bash
az containerapp logs show \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--follow
```
### Cleanup
If you want to get rid of everything again that you created in this CLI walkthrough, you can delete all resources like so:
```bash
az group delete --name $RESOURCE_GROUP_NAME --yes --no-wait
```
## Connecting to Azure Artifacts (managed identity)
Enable a system-assigned managed identity:
```bash
az containerapp identity assign \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--system-assigned
```
This prints the identity’s `principalId`. Grant it access to the feed as described in the [portal guide](azure-container-apps.md#connecting-to-azure-artifacts-optional), then set the environment variables:
```bash
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--set-env-vars \
XLWINGS_PYPI_INDEX_URL='https://pkgs.dev.azure.com//_packaging//pypi/simple/' \
XLWINGS_PYPI_AUTH_MODE='azure-managed-identity'
```
## Updating
To update xlwings Lite, point your container app at the new image tag:
```bash
az containerapp update \
--name $APP_NAME \
--resource-group $RESOURCE_GROUP_NAME \
--image xlwings/xlwings-lite:1.0.0.104
```
# azure-container-apps.md
# Azure Container Apps
This guide shows how to deploy xlwings Lite to Azure Container Apps.
## Deploying the Container
1. Go to [Container Apps in the Azure portal](https://portal.azure.com/#browse/Microsoft.App%2FcontainerApps).
2. **+ Create** > **+ Container App**
- Select the desired **Resource group** (you may need to click on Create new resource group first)
- Container app name: e.g. `xlwings-lite`
- Deployment source: **Container image**
- Select the desired **Region**
- Select the desired **Container Apps environment** (you may need to click on Create new environment first)
3. Click on **Next: Container**
- Image source: **Docker Hub or other registries**
- Image type: **Public**
- Registry login server: `docker.io`
- Image and tag: `xlwings/xlwings-lite:1.0.0.104`
- CPU and memory: `0.5 CPU cores, 1 Gi memory`
- Environment variables: `XLWINGS_LICENSE_KEY`: `your-license-key` (if you don’t have one, get a [xlwings trial license key](https://www.xlwings.org/trial))
4. Click on **Next: Ingress**
- Activate the checkbox for **Ingress**
- Under **Ingress traffic**, select **Accepting traffic from anywhere**
- Set **Target port** to `8000`
- Click on **Next: Tags**
- Click on **Next: Review + create**
- Click on **Create**
5. You can optionally restrict access under **Networking** > **IP Restrictions**.
6. Under **Application** > **Scale** (or **Scale and replicas**), set both **Min replicas** and **Max replicas** to `1` to prevent cold starts. If desired, you could set a **Scale rule** to set **Min replicas** to `0` outside business hours.
7. Go to your **Application Url**. You should see the version of xlwings Lite.
## Connecting to Azure Artifacts (optional)
By default, xlwings Lite installs packages from the public PyPI. If your organization keeps its Python packages in an [Azure Artifacts](https://learn.microsoft.com/en-us/azure/devops/artifacts/) feed, you can point xlwings Lite at it instead by setting `XLWINGS_PYPI_INDEX_URL`. The container reverse-proxies the feed on the same origin (under `/pypi/`), so the browser never sees the feed’s credentials and there are no CORS or CSP exceptions to manage.
There are two ways to authenticate the container to the feed:
- **Managed identity (recommended):** no secret is stored anywhere. The container fetches a short-lived Microsoft Entra token from the Azure platform at runtime. This is the cleaner option and the one documented in full below.
- **Personal Access Token (PAT):** a token embedded in the index URL. Simpler to set up, but the token expires and must be rotated. See [Alternative: Personal Access Token (PAT)]().
#### NOTE
This section assumes you already have a working deployment from the steps above. The feed URL is the same one you would pass to `pip install --index-url`, e.g. `https://pkgs.dev.azure.com///_packaging//pypi/simple/` (organization-scoped feeds omit the `/` segment).
### Managed identity
#### 1. Enable a system-assigned managed identity
1. Go to your container app, then **Settings** > **Security** > **Identity**.
#### NOTE
In older portal layouts the **Identity** blade lives directly under **Settings** rather than under **Security**. If you can’t find it, use the search box at the top of the left-hand menu and type `Identity`.
2. On the **System assigned** tab, switch **Status** to **On** and click **Save**.
3. Note the **Object (principal) ID** that appears — you’ll need it in the next step.
#### 2. Grant the identity access to the feed
The managed identity is a Microsoft Entra service principal. Before you can grant it feed permissions, it usually has to be added to the Azure DevOps organization first.
1. In Azure DevOps, go to **Organization settings** > **Users** > **Add users**.
2. Set the user type to **Service Principal**, search by the **Object (principal) ID** (or the identity’s **Application/Client ID**), and assign an access level (e.g. **Basic** or **Stakeholder**). Click **Add**.
#### NOTE
This step is easy to miss. A brand-new managed identity often does **not** show up by name in the feed’s permissions dialog until it has been added to the organization here.
3. Now open your feed > **Feed settings** (gear icon) > **Permissions** > **Add users/groups**, find the identity (search by name or by the Object ID), and assign the role **Collaborator** (shown as **Feed and Upstream Reader (Collaborator)**).
#### WARNING
Use **Collaborator**, not **Reader**. Reader can only serve packages that are already cached in the feed. Installing a package that isn’t cached yet triggers an ingestion from the feed’s upstream source (e.g. public PyPI), which requires the **Collaborator** role or higher. With only **Reader**, uncached packages fail with a misleading `Cannot find the package ... in feed` (`PackageNotFound`) error.
#### 3. Configure the environment variables
Go to **Application** > **Containers** > **Environment variables** (**Edit and deploy**) and add:
- `XLWINGS_PYPI_INDEX_URL`: your feed URL, e.g. `https://pkgs.dev.azure.com//_packaging//pypi/simple/` (no token)
- `XLWINGS_PYPI_AUTH_MODE`: `azure-managed-identity`
Saving creates a new revision and restarts the container.
#### NOTE
Enable the managed identity (step 1) **before** setting these variables. The platform only injects the identity token endpoint into the container once an identity is assigned, and xlwings Lite reads it at startup. If you set the variables first, restart the revision after enabling the identity.
#### 4. Verify
Open `https:///pypi/simple//` in a browser (or with `curl`). A `200` response listing the package’s files (as an HTML page of links, or as JSON if you request it with `Accept: application/vnd.pypi.simple.v1+json`) means the feed connection and authentication are working. You can then add the package to your `requirements.txt` in the add-in.
If you get a `401`/`403`, check the identity’s feed permissions (step 2). If a never-installed package returns a `404` with `PackageNotFound`, the identity likely has **Reader** instead of **Collaborator**. The container logs (**Monitoring** > **Log stream**, or `az containerapp logs show`) report token-fetch failures explicitly.
### Alternative: Personal Access Token (PAT)
If you can’t use a managed identity, embed a PAT in the feed URL instead. Create a PAT in Azure DevOps (**User settings** > **Personal access tokens**) with the **Packaging: Read** scope, then set a single environment variable:
- `XLWINGS_PYPI_INDEX_URL`: `https://@pkgs.dev.azure.com//_packaging//pypi/simple/`
No `XLWINGS_PYPI_AUTH_MODE` is needed (it defaults to PAT/basic authentication). The proxy forwards the token to the feed and strips it from everything the browser sees.
#### NOTE
As with managed identity, installing an uncached package triggers upstream ingestion, so the PAT (or the account it belongs to) needs permission to save packages from upstream sources. Prefer a token from a dedicated service account over a personal one, and rotate it before it expires.
## Register the add-in with Microsoft 365 admin center
1. In your browser, go to `https:///manifest`, which will download `xlwings-lite-manifest.xml`.
2. Go to [Microsoft 365 admin center](https://admin.microsoft.com/)
- Click on [`Show all` > `Settings` > `Integrated Apps`](https://admin.microsoft.com/#/Settings/IntegratedApps).
- If you have **xlwings Lite** installed, uninstall it first.
- Click on `Upload custom apps` and select `Office Add-in` (App type).
- Select `Upload manifest file (.xml) from device`. Click `Choose File`, then select the `xlwings-lite-manifest.xml` from the previous step.
- Click `Next`, then assign the desired users.
- Click `Next` and accept permission requests.
- Click `Next` and `Finish deployment`.
The users will get the add-in to show up automatically although it may take a few hours.
#### NOTE
If you want to remove the add-in again and run into issues (“Remove apps failed. No apps were successfully removed. Please try to remove them later.”), use this legacy URL: [https://admin.microsoft.com/#/Settings/AddIns](https://admin.microsoft.com/#/Settings/AddIns)
## Updating
To update xlwings Lite, point your container app at the new image tag.
1. Go to your container app, then **Application** > **Containers**.
2. Click **Edit and deploy**, select the container, and update the **Image and tag** to:
```text
xlwings/xlwings-lite:1.0.0.104
```
3. Click **Save** to create a new revision and deploy it.
#### NOTE
Normally you don’t need to update the manifest after deploying a new version of the container. This would only be required, e.g., if the URL of your container changed. In that case, the Microsoft 365 admin center offers a link to **Update Add-in**.
# changelog.md
# Changelog
## 1.0.0.103 (August 18, 2026)
- Custom functions can now access the calling cell by adding an argument with the `xw.Caller` type hint, see [Accessing the calling cell](custom-functions.md#accessing-the-calling-cell). It provides the address, row/column and sheet/book name of the cell that called the function. Requires xlwings>=0.36.17.
- Custom functions can now run a script after they return by wrapping their return value in `xw.WithScript()`. This allows a custom function to trigger side effects such as writing to other cells, see [Running a script after a custom function](custom-functions.md#running-a-script-after-a-custom-function). Requires xlwings>=0.36.17.
## 1.0.0.102 (Aug 14, 2026)
- Returning Matplotlib figure objects from custom functions is now enabled by default. This uploads the rendered image to the xlwings image cache during calculation; you can turn it off under **Settings** > **Local**, see [Plots](custom-functions.md#plots). Self-hosted deployments are unaffected as they don’t use an external image cache.
## 1.0.0.101 (Aug 12, 2026)
- Notebooks: **View in Excel (new sheet)** now names the new sheet after the variable (e.g. `df`, `df1`, etc.) instead of `view1`, `view2`, etc., see [View in Excel](notebooks.md#view-in-excel).
- Editor: right-clicking an editor tab now allows you to **Close All** or **Close Others**.
## 1.0.0.100 (Aug 11, 2026)
- To enable reproducible workbooks, all dependencies and sub-dependencies are now automatically pinned in `requirements-pinned.txt`, resolved from `requirements.txt`. This requires Pyodide 314.0.0+, see [Dependencies](dependencies.md).
- Removed `python-dotenv` as a required dependency.
## 1.0.0.98 (Aug 2, 2026)
- Custom functions can now appear under a different name in Excel than the Python function name via `@func(name="myName")`, see [Custom function name](custom-functions.md#custom-function-name).
- A custom-function namespace can now be set for an entire Python module via `__xlwings_func_namespace__`, see [Namespace](custom-functions.md#namespace).
## 1.0.0.97 (Jul 31, 2026)
- Scripts now support arguments. In App Mode, they will be rendered into a form, using appropriate form elements such as a text box, a dropdown menu, a checkbox or a date/time picker, see [Script arguments](custom-scripts.md#script-arguments).
## 1.0.0.96 (Jul 29, 2026)
- Notebooks: added support for the following plotting libraries: Bokeh, Altair, xy, and Plotnine.
- The Output pane can now optionally be shown in App Mode (under **Settings** > **Workbook** > **App Mode**).
- The Output pane now supports colors, e.g., by using Colorama or Rich.
## 1.0.0.95 (Jul 28, 2026)
This release introduces support for multiple source files:
#### NOTE
This release will trigger the trust screen (“Do you trust the Authors of this workbook?”) for any existing workbook.
- You can now create, rename, delete, open, and close source files via the new **Source files** menu (on the left of the editor tabs).
- File system layout: source files now live under `/src`, and the working directory is `/data`. Files that your code writes with a relative path (e.g., `open("output.json", "w")`) therefore persist and appear under **Files** > **Import/Export**. To read a source file from code, use `Path(__file__).parent / "config.json"`, see [Files](files.md).
- Notebooks: Notebooks now have the `.nb.py` file extension; create them with **New Notebook…** from the **Source files** menu. New workbooks ship a `sample.nb.py` instead of the legacy `notebook.py`. Legacy `notebook.py` keep working unchanged.
- pytest: The test runner now collects multiple test files following pytest’s convention: any `test_*.py` or `*_test.py` file. Create them with **New File…**. See [Testing with Pytest](tests.md). **Breaking:** the legacy `tests.py` file is no longer auto-created, and is no longer collected by the runner. A legacy `tests.py` has to be renamed into e.g. `test_main.py`. `pytest` is also no longer included in the default `requirements.txt`. Add `pytest` manually and restart to use the test runner.
Other changes:
- Scripts: you can now additionally run any script directly from its play button in the editor gutter.
## 1.0.0.93 (Jul 16, 2026)
Improved notebook experience:
- Cell status (running, success, error) is now shown to the left of the line numbers.
- Right-click a variable > **View in Excel (new sheet)** to view its data in a new sheet.
- New cell actions: **Run Above** and **Delete Cell**.
- Before running a cell, the current workbook state is now loaded automatically so that `book.sheets.active` etc. resolve correctly.
- `Shift+Enter` on the last cell now adds an empty cell at the end, in line with how it works in Jupyter notebooks.
- Notebooks now recommend the [Async API](async-api.md) for better performance with large workbooks.
For more details, see [Notebooks](notebooks.md).
## 1.0.0.91 (Jul 13, 2026)
- Fixed a regression that restricted the available Pyodide versions to only one.
## 1.0.0.89 (Jul 10, 2026)
- [Self-hosted]: New workbooks now default to Pyodide 314.0.1.
## 1.0.0.88 (Jul 1, 2026)
- Custom functions that return Matplotlib figures are now officially supported. Note that this requires xlwings>=0.36.8 in `requirements.txt`. See [Plots](custom-functions.md#plots) for the details.
## 1.0.0.0-82 (Jun 18, 2026)
- Pyodide 314.0.0 (Python 3.14.2) is now the default version for new workbooks.
- Various bug fixes for streaming functions, object handles, and Pyodide 314.0.0.
## 1.0.0.0-80 (Jun 12, 2026)
- Added support for object handles, see [Object handles](custom-functions.md#object-handles).
## 1.0.0.0-78 (Jun 9, 2026)
- Added support for Pyodide 314.0.0. It’s not the default Pyodide version yet, but you can select it under Settings > Workbook.
## 1.0.0.0-77 (Jun 6, 2026)
- [Self-hosted]: added support for Azure Artifacts.
## 1.0.0.0-76 (Jun 3, 2026)
- Support streaming functions, see [Streaming functions ("RTD functions")](custom-functions.md#streaming-functions-rtd-functions)
## 1.0.0.0-74 (May 28, 2026)
- Fix occasional hanging during startup.
- Fix scroll direction of the editor tabs.
## 1.0.0.0-69 (May 21, 2026)
- Fix Paste in the context menu (right-click) of the editor.
## 1.0.0.0-65 (May 6, 2026)
- Custom functions: if a string parameter only accepts a fixed set of values, you can declare it with `typing.Literal` so that Excel will show a dropdown of those values as the user types the function:, see [Enum parameters](custom-functions.md#enum-parameters).
## 1.0.0.0-64 (May 5, 2026)
- [Self-hosted]: Bug Fix for “convert from cloud add-in” functionality.
## 1.0.0.0-63 (May 4, 2026)
- [Self-hosted]: default to `XLWINGS_ENABLE_PYPI=true`.
## 1.0.0.0-62 (May 3, 2026)
- Removed the Python REPL (use `notebook.py` instead).
## 1.0.0.0-61 (May 2, 2026)
- Dark mode is here!
## 1.0.0.0-60 (May 1, 2026)
- [Self-hosted]: added “convert from cloud add-in” functionality.
## 1.0.0.0-58 (Apr 30, 2026)
- [Self-hosted]: show icons in the Office 365 admin center.
- Bug Fix: all files are now stored in the workbook, even if they contain the default content. This prevents e.g., silent version upgrades in `requirements.txt`.
## 1.0.0.0-57 (Apr 27, 2026)
- Added a new `test.py` file to run pytest tests, see [Testing with Pytest](tests.md).
## 1.0.0.0-52 (Apr 16, 2026)
- Introduced a menu item to Import Python in Excel code into `notebook.py`.
## 1.0.0.0-50 (Apr 13, 2026)
- Plotly plots are now supported in `notebook.py`.
## 1.0.0.0-49 (Apr 10, 2026)
- Bug Fix: Calling `await book.flush()` could result in `PropertyNotLoaded` errors.
- Show previously hidden errors in the Problems pane.
- New workbooks now default to xlwings 0.35.1.
## 1.0.0.0-46 (Apr 6, 2026)
- Introduced `notebook.py`, similar to a Jupyter notebook
- Deprecated `await Book.sync()` in favor of `await Book.flush()` to make it clearer what it does.
- Introduced a new async API, see [Async API](async-api.md).
- New workbooks now default to xlwings 0.35.0.
## 1.0.0.0-45 (Mar 27, 2026)
- Better autocomplete.
- Code issues are now shown with red squiggly lines via [Ty](https://docs.astral.sh/ty/) and listed in the Problems pane.
- Enabled Go to Definition and Rename Symbol (right-click in the editor).
- New workbooks now default to xlwings 0.34.0.
## 1.0.0.0-43 (Mar 22, 2026)
- Added support for `Range.to_png()`.
- Use Ruff instead of Black for code formatting via `Shift+Alt+F`, configurable via `pyproject.toml`.
- New method `await book.sync()`: Use this in `async` functions to process the xlwings commands instantly and to flush `print()` to the Output pane. This is required if an xlwings command produces a file that you need before the end of the `script`, e.g., if you want to use the PNG file produced by `Range.to_png()`.
- New workbooks now default to xlwings 0.33.22.
## 1.0.0.0-39 (Mar 12, 2026)
- App Mode now shows exceptions in an alert.
- Bug Fix: always show scripts in order of appearance from the top.
## 1.0.0.0-38 (Mar 11, 2026)
- Output pane: new `Clear` button and various bug fixes.
## 1.0.0.0-37 (Mar 9, 2026)
- New App mode, allowing for deployment to end-users without showing the editor with the Python code, see [App Mode](app-mode.md).
- `@script` decorators now accept a `name` argument to label the button differently from the function name: `@script(name="My Button")`.
- Wingman chat sessions aren’t automatically resumed after starting the add-in, only within the same add-in session.
## 1.0.0.0-36 (Mar 7, 2026)
- The Pyodide version can now be changed under `Settings` > `Workbook`. For now, there’s just one version, but going forward, new Pyodide versions will be added.
- The sample code can now be switched off under `Settings` > `Local` so the editor starts with a blank file and without the optional dependencies like pandas, etc.
- Changed the autostart default under `Settings` > `Workbook` to `Autostart and open task pane`.
- Improved Wingman response quality.
## 1.0.0.0-35 (Feb 23, 2026)
- Added Wingman Chat in the Output pane. Use it to write or explain your code. It supports Claude, Gemini, and OpenAI out of the box (bring your own API key) plus any OpenAI API compatible API that runs on https and supports CORS.
## 1.0.0.0-34 (Feb 21, 2026)
- Wingman: added provider support for Claude by Anthropic.
- Personal modules: automatically load scripts and custom functions across all workbooks from local files similar to how `Personal.xlsb` works with VBA, see [Personal Modules](personal-modules.md).
## 1.0.0.0-30 (Feb 13, 2026)
- Added the built-in custom function `=WINGMAN(prompt1, [prompt2], ...)`, equivalent to Microsoft’s `COPILOT`. `WINGMAN` is a bring-your-own-model solution: connect it with your OpenAI, Gemini, or other models via xlwings Lite menu > Settings > Local > Wingman.
## 1.0.0.0-29 (Feb 10, 2026)
- xlwings Lite now has a Python Console (REPL). It automatically installs the packages from `requirements.txt` but currently doesn’t support the imported files or mounted local folders.
## 1.0.0.0-28 (Feb 3, 2026)
- **Trusted workbooks**: To keep your environment variables and files secure from attacks, you must now trust a workbook when you run it for the first time if you didn’t author it yourself. You’ll need to do this for existing workbooks, even if you wrote them yourself. You’ll also need to re-trust them whenever the Office cache is cleared, which happens occasionally.
- **Autostart**: The add-in now starts automatically in the background. You can change this in the workbook settings to disable autostart or to start the add-in and show the task pane. Note that existing workbooks will only autostart after you open the add-in manually once more and then save the workbook.
- **Local Files**: Via menu > `Files`, You can now import/export files to access them under `/data`, all handled locally. On Windows Desktop, you can additionally mount a local directory to read from and write to your local disk directly.
## 1.0.0.0-27 (Jan 31, 2026)
- Bug Fix: The content of `main.py` was sometimes replaced with the default code samples or content from `requirements.txt`.
## 1.0.0.0-24 (Jan 22, 2026)
- Fixed an issue with xlwings 0.33.20.
- New workbooks now default to xlwings 0.33.20.
## 1.0.0.0-21 (Jan 18, 2026)
- When using sheet buttons, you can now select a specific cell in your script without it being overridden anymore.
- New workbooks now default to xlwings 0.33.19.
## 1.0.0.0-18 (Jul 11, 2025)
- Show a security warning with regard to using environment variables with scope “Add-in”.
- New workbooks now default to xlwings 0.33.15.
## 1.0.0.0-17 (Jun 23, 2025)
- If you are self-hosting xlwings Lite, you can now build your own Docker image to include custom packages.
## 1.0.0.0-16 (May 27, 2025)
- xlwings Lite is now available as Docker image for [self-hosting](self-hosting.md).
## 1.0.0.0-15 (May 3, 2025)
- Added support for [sheet buttons](custom-scripts.md#running-a-script-via-sheet-button).
- Added support for [scripts configuration](custom-scripts.md#script-configuration) via `include` and `exclude` arguments to prevent sending over the full workbook data to Python.
- New workbooks now default to xlwings 0.33.14.
If you want to use these new features with an existing workbook, you need to upgrade to xlwings 0.33.14 or later via `requirements.txt` tab, followed by a restart.
## 1.0.0.0-14 (Apr 25, 2025)
- Added support for [environment variables](environment-variables.md).
## 1.0.0.0-13 (Apr 18, 2025)
- `Range.font` properties can now be set (`bold`, `italic`, `color`, `size`, `name`).
- Polars DataFrame and Series are now supported via native converter.
- Images aren’t selected anymore after inserting/updating.
- Bug fix: Scripts wouldn’t run if e.g., a shape was selected.
- Bug fix: `Range.expand()` was stopping at `0` values.
- New workbooks now default to xlwings 0.33.12.
If you want to use these new features with an existing workbook, you need to upgrade to xlwings 0.33.12 or later via `requirements.txt` tab, followed by a restart.
## 1.0.0.0-12 (Apr 6, 2025)
- Upgraded to Pyodide 0.27.5, giving access to the `openai` and `osqp` packages amongst others.
- Made the dependency `pyodide-http` optional.
## 1.0.0.0-11 (Mar 25, 2025)
- Show full error tracebacks in the output pane instead of just the error message in an alert.
## 1.0.0.0-10 (Mar 23, 2025)
- Bug fix: Fixed an occasional bug with loading Pyodide.
## 1.0.0.0-9 (Mar 22, 2025)
- Bug fix: Show errors in the output pane whenever changes are saved and prevent running a previously working version of the script/function.
## 1.0.0.0-8 (Mar 21, 2025)
- First public release.
# cleanup-missing-addin-refs.md
# Clean Up Missing Add-in Refs
After uninstalling a separate installation of xlwings Lite (SH), existing workbooks might cause the following error:
```text
ADD-IN ERROR
The document is missing information about this add-in. Add-in name: xlwings Lite (SH)
```
This guide shows how to use the **Clean up missing add-ins refs** feature to fix this.
## Clean up existing workbooks
1. Make a backup of all the workbooks you want to clean up.
2. Open a new, empty workbook, then open **xlwings Lite (SH)**. Make sure you don’t have any of the workbooks open that you want to clean up.
3. Go to **≡ > Files** and select the **Import/Export** tab. Drag and drop one or more Excel files (or folders with Excel files) onto the drop zone.

4. Click on **≡ > Clean up missing add-ins refs**.

5. Confirm the prompt in the Output pane. Any errors during clean up will be reported there.

6. Once done, go back to **≡ > Files > Import/Export** tab and download the now cleaned file(s) either individually or by clicking on **Download All**.

7. If you used **Download All**, you need to unzip `data.zip` (right-click > **Extract all…** > **Extract**) and replace your local workbooks with the version from the unzipped `data` folder. If you downloaded the file individually, no unzipping is required.
# configuration.md
# Configuration
xlwings Lite is configured via the following settings. You set them either as environment variables or by using the `xlwings-lite` CLI when deploying on a Linux VM. `XLWINGS_LICENSE_KEY` is required; all others are optional.
## General
| Variable | Default | Description |
|-----------------------|-----------|------------------------------------------------------------------------------------------------------------|
| `XLWINGS_LICENSE_KEY` | — | Your xlwings license key (request a [trial key](https://www.xlwings.org/trial)). |
| `XLWINGS_HOSTNAME` | — | e.g., `xlwings-lite.mycompany.com`. Set this if the downloaded `manifest.xml` doesn’t use the correct URL. |
| `PORT` | `8000` | Sets the port on which the container, i.e., application runs. |
## Python packages
| Variable | Default | Description |
|-----------------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `XLWINGS_ENABLE_PYPI` | `true` | If `true`, allows installing packages directly from [https://pypi.org/](https://pypi.org/). Ignored when `XLWINGS_PYPI_INDEX_URL` is set. |
| `XLWINGS_PYPI_INDEX_URL` | — | Full URL of a private PyPI index (e.g., Nexus, Artifactory, devpi, Azure Artifacts) — the same URL you’d pass to `pip install --index-url`. Example: `https://nexus.example.com/repository/pypi-proxy/simple/`. For an authenticated index, the credential can be embedded in the URL (e.g., a token: `https://@host/.../simple/`); it is forwarded to the index and never exposed to the browser. |
| `XLWINGS_PYPI_AUTH_MODE` | `basic` | How the container authenticates to `XLWINGS_PYPI_INDEX_URL`. `basic` uses credentials embedded in the URL (or none). Set to `azure-managed-identity` to authenticate to an Azure Artifacts feed using the Azure platform’s managed identity instead of a token — the container fetches a short-lived Microsoft Entra token at runtime, so no secret is stored. Requires hosting on Azure, see [Azure Container Apps guide](azure-container-apps.md#connecting-to-azure-artifacts-optional). |
| `XLWINGS_PYODIDE_DEFAULT_VERSION` | `314.0.1` | The Pyodide version used for newly created workbooks. Existing workbooks keep the version they were pinned to. Available versions: `0.27.5`, `314.0.1`. |
## CORS proxy
| Variable | Default | Description |
|--------------------------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `XLWINGS_CORS_PROXY_HOSTS` | — | Comma-separated allowlist of hostnames that user Python code can reach via the built-in CORS proxy. Supports exact matches and `*.domain.com` wildcards (e.g., `api.example.com,*.trusted.com`). HTTPS-only. Unset (the default) disables the proxy. |
| `XLWINGS_CORS_PROXY_ALLOW_ALL_HOSTS` | `false` | If `true`, proxies all requests, ignoring `XLWINGS_CORS_PROXY_HOSTS`. |
## Tooling
| Variable | Default | Description |
|----------------------------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `XLWINGS_ENABLE_MISSING_ADDIN_CLEANUP` | `true` | If `true`, exposes a “Clean up missing add-in references” menu item that strips orphaned add-in references from workbooks in `/data`. Only required if workbooks that were created with the cloud add-in complain about a missing add-in. |
# custom-functions.md
# Custom Functions
This tutorial teaches you everything about custom functions. Some highlights:
- [pandas]() and [Polars]() DataFrames as arguments and return values
- [Object handles]() to return Python objects to a single cell
- [Plots](): return Matplotlib figures as images
- [Streaming functions]() for live-updating values such as financial market data
## Basic syntax
The simplest custom function only requires the `@func` decorator:
```python
from xlwings import func
@func
def hello(name):
return f"Hello {name}!"
```
In Excel, you can call this function by typing `=HELLO("World")` into a cell. You could also type `World` into cell `A1`, then reference that cell from the formula like so: `=HELLO(A1)`.
Custom functions can be defined in `main.py` or any other regular `.py` file in
the workbook. Custom function names need to be unique, so
defining the same function name in multiple Python modules results in a
registration error (Problems pane) that identifies both files. If a workbook function has the
same name as one in a [personal module](personal-modules.md), the workbook function takes precedence.
By default, function arguments that are single cells (like the `name` argument in the above example) arrive as a simple float, integer, boolean, or string. One-dimensional Excel ranges arrive as a list of values (e.g., `[1, 2]`), and two-dimensional ranges arrive as a list of lists (e.g., `[[1, 2], [3, 4]]`). For more details, see [Dimension of arguments](). The next section describes how to have the values arrive as pandas DataFrame instead.
## pandas DataFrames
By using the `@arg` and `@ret` decorators, you can apply converters and options to arguments and the return value, respectively.
For example, to read in the values of a range as pandas DataFrame and return the correlations without writing out the header and the index, you would write:
```python
import pandas as pd
from xlwings import func, arg, ret
@func
@arg("df", pd.DataFrame)
@ret(index=False, header=False)
def correl2(df):
return df.corr()
```
## Polars DataFrames
Polars DataFrames work almost the same as pandas DataFrames. But since polars DataFrames don’t have an index and don’t support MultiIndex headers, the `index` option isn’t available and the `header` option only accepts `True` (default) or `False` (note that this example uses type hints, see [next section]()):
```python
import polars as pl
@xw.func
def myfunction(df: pl.DataFrame):
# df is a polars DataFrame, do something with it
return df
```
For an overview of the available converters and options, have a look at [Converters and Options](https://docs.xlwings.org/en/latest/converters.html).
## Using type hints instead of decorators
You can use type hints instead of or in combination with decorators:
```python
from xlwings import func
import pandas as pd
@func
def myfunction(df: pd.DataFrame) -> pd.DataFrame:
# df is a DataFrame, do something with it
return df
```
In this example, the return type (`-> pd.DataFrame`) is optional, as xlwings automatically checks the type of the returned object.
If you need to provide additional conversion arguments, you can either provide them via an annotated type hint or via a decorator. Note that when you use type hints and decorators together, decorators override type hints for conversion.
To set `index=False` for both the argument and the return value, you can annotate the type hint like this:
```python
from typing import Annotated
from xlwings import func
import pandas as pd
@func
def myfunction(
df: Annotated[pd.DataFrame, {"index": False}]
) -> Annotated[pd.DataFrame, {"index": False}]:
# df is a DataFrame, do something with it
return df
```
As this might be a little harder to read, you can extract the type definition, which also allows you to reuse it like so:
```python
from typing import Annotated
from xlwings import func
import pandas as pd
Df = Annotated[pd.DataFrame, {"index": False}]
@func
def myfunction(df: Df) -> Df:
# df is a DataFrame, do something with it
return df
```
Alternatively, you could also combine type hints with decorators:
```python
from typing import Annotated
from xlwings import func, arg, ret
import pandas as pd
@func
@arg("df", index=False)
@ret(index=False)
def myfunction(df: pd.DataFrame) -> pd.DataFrame:
# df is a DataFrame, do something with it
return df
```
## Variable number of arguments (`*args`)
Varargs are supported. You can also use a converter, which will be applied to all arguments provided by `*args`:
```python
from xlwings import func, arg
@func
@arg("*args", pd.DataFrame, index=False)
def concat(*args):
return pd.concat(args)
```
and the same with type hints:
```python
from typing import Annotated
from xlwings import func
@func
def concat(*args: Annotated[pd.DataFrame, {"index": False}]):
return pd.concat(args)
```
## Doc strings
To describe your function and its arguments, you can use a function docstring or the `arg` decorator, respectively:
```python
from xlwings import func, arg
@func
@arg("name", doc='A name such as "World"')
def hello(name):
"""This is a classic Hello World example"""
return f"Hello {name}!"
```
And again with type hints:
```python
from typing import Annotated
from xlwings import func
@func
def hello(name: Annotated[str, {"doc": 'A name such as "World"'}]):
"""This is a classic Hello World example"""
return f"Hello {name}!"
```
These doc strings will appear in Excel’s function wizard/formula builder. Note that the name of the arguments will automatically be shown when typing the formula into a cell without having to do anything (intellisense).
## Enum parameters
If a string parameter only accepts a fixed set of values, you can declare it with `typing.Literal` so that Excel will show a dropdown of those values as the user types the function:
```python
from typing import Literal
from xlwings import func
@func
def get_planet(name: Literal["mercury", "venus", "earth"]) -> str:
return name.title()
```
#### NOTE
Only string Literals show a dropdown. `Literal[1, 2, 3]` is accepted as a type hint but no dropdowns are shown in Excel.
To attach per-value tooltips (shown in Excel’s AutoComplete dropdown), wrap the `Literal` in `Annotated` with a `tooltips` dict:
```python
from typing import Annotated, Literal
from xlwings import func
@func
def get_planet_with_tooltips(
name: Annotated[
Literal["mercury", "venus", "earth"],
{
"tooltips": {
"mercury": "Mercury is the first planet from the sun.",
"venus": "Venus is the second planet from the sun.",
"earth": "Earth is the third planet from the sun.",
},
},
],
) -> str:
return name.title()
```
#### NOTE
- Tooltips aren’t shown on macOS.
- Requires xlwings 0.35.3+
## Date and time
Depending on whether you’re reading from Excel or writing to Excel, there are different tools available to work with date and time.
### Reading date and time
In the context of custom functions, xlwings will detect numbers, strings, and booleans but not cells with a date/time format. Hence, you need to use converters. For single datetime arguments do this:
```python
import datetime as dt
from xlwings import func
@func
@arg("date", dt.datetime)
def myfunc(date):
return date
```
And again with type hints:
```python
import datetime as dt
from xlwings import func
@func
def myfunc(date: dt.datetime):
return date
```
Instead of `dt.datetime`, you can also use `dt.date` to get a date object instead.
If you have multiple values that you need to convert, you can use the `xlwings.to_datetime()` function:
```python
import datetime as dt
import xlwings as xw
from xlwings import func
@func
def myfunc(dates):
dates = [xw.to_datetime(d) for d in dates]
return dates
```
And if you are dealing with pandas DataFrames, you can simply use the `parse_dates` option. It behaves the same as with `pandas.read_csv()`:
```python
import pandas as pd
from xlwings import func, arg
@func
@arg("df", pd.DataFrame, parse_dates=[0])
def timeseries_start(df):
return df.index.min()
```
and again with type hints:
```python
from typing import Annotated
import pandas as pd
from xlwings import func
@func
def timeseries_start(df: Annotated[pd.DataFrame, {"parse_dates": [0]}]):
return df.index.min()
```
Like `pandas.read_csv()`, you could also provide `parse_dates` with a list of columns names instead of indices.
### Writing date and time
When writing datetime object to Excel, xlwings automatically formats the cells as date if your version of Excel supports data types, so no special handling is required:
```python
import datetime as dt
import xlwings as xw
from xlwings import func
@func
def pytoday():
return dt.date.today()
```
By default, it will format the date according to default format of Excel, but you can also override this by providing the `date_format` return option:
```python
import datetime as dt
import xlwings as xw
from xlwings import func
@func
@ret(date_format="yyyy-m-d")
def pytoday():
return dt.date.today()
```
and again with type hints:
```python
import datetime as dt
import xlwings as xw
from xlwings import func
@func
def pytoday() -> Annotated[dt.date, {"date_format": "yyyy-m-d"}]:
return dt.date.today()
```
For the accepted `date_format` string, consult the [official Excel documentation](https://support.microsoft.com/en-us/office/format-numbers-as-dates-or-times-418bd3fe-0577-47c8-8caa-b4d30c528309).
#### NOTE
Some older builds of Excel don’t support date formatting and will display the date as date serial instead, requiring you format it manually.
## Custom function name
By default, a custom function appears in Excel under the upper-cased Python function name. Use the `name` argument if the function should appear under a different name (analogous to how it works with scripts):
```python
from xlwings import func
@func(name="helloName")
def hello_custom_name(name):
return f"Hello {name}!"
```
This function will be shown as `helloName` in Excel. The name must start with a letter and may only contain letters, numbers, periods, and underscores (max. 128 characters).
#### NOTE
The `name` argument requires xlwings 0.36.12+ and is only supported by xlwings Lite and xlwings Server. The classic xlwings add-in on Windows ignores it and registers the function under its Python name.
## Namespace
A namespace groups related custom functions together by prepending the namespace to the function name, separated with a dot. For example, to have NumPy-related functions show up under the numpy namespace, you could do:
```python
import numpy as np
from xlwings import func
@func(namespace="numpy")
def standard_normal(rows, columns):
rng = np.random.default_rng()
return rng.standard_normal(size=(rows, columns))
```
This function will be shown as `NUMPY.STANDARD_NORMAL` in Excel.
To use the same namespace for all custom functions in a Python module, set
`__xlwings_func_namespace__` at module level:
```python
from xlwings import func
__xlwings_func_namespace__ = "numpy"
@func
def standard_normal(rows, columns):
...
@func
def mean(values):
...
```
These functions will be shown as `NUMPY.STANDARD_NORMAL` and `NUMPY.MEAN`. A
`namespace` passed directly to `@func` takes precedence over the module-level
namespace. Module-level namespaces require xlwings 0.36.13 or later.
To exclude an individual function from the module-level namespace, use
`@func(namespace="")`.
### Sub-namespace
You can create sub-namespaces by including a dot like so:
```python
@func(namespace="numpy.random")
```
This function will be shown as `NUMPY.RANDOM.STANDARD_NORMAL` in Excel.
## Help URL
You can include a link to a web page with more information about your function by using the `help_url` option. The function wizard/formula builder will show that link under “More help on this function”.
```python
from xlwings import func
@func(help_url="https://www.xlwings.org")
def hello(name):
return f"Hello {name}!"
```
## Array Dimensions
If you want your function to accept arguments of any dimensions (as single cell or one- or two-dimensional ranges), you may need to use the `ndim` option to make your code work in every case. Likewise, you can return a simple list in a vertical orientation by using the `transpose` option.
### Dimension of arguments
Depending on the dimensionality of the function parameters, xlwings either delivers a scalar, a list, or a nested list:
- Single cells (e.g., `A1`) arrive as scalar, i.e., number, string, or boolean: `1` or `"text"`, or `True`
- A one-dimensional (vertical or horizontal!) range (e.g. `A1:B1` or `A1:A2`) arrives as list: `[1, 2]`
- A two-dimensional range (e.g., `A1:B2`) arrives as nested list: `[[1, 2], [3, 4]]`
This behavior is not only consistent in itself, it’s also in line with how NumPy works and is often what you want: for example, you can directly loop over a vertical 1-dimensional range of cells.
However, if the argument can be anything from a single cell to a one- or two-dimensional range, you’ll want to use the `ndim` option: this allows you to always get the inputs as a one- or two-dimensional list, no matter what the input dimension is:
```python
from xlwings import func, arg
@func
@arg("x", ndim=2)
def add_one(x):
return [[cell + 1 for cell in row] for row in data]
```
and again with type hints:
```python
from typing import Annotated
from xlwings import func
@func
def add_one(x: Annotated[float, {"ndim": 2}]):
return [[cell + 1 for cell in row] for row in data]
```
The above sample would raise an error if you’d leave away the `ndim=2` and use a single cell as argument `x`.
### Dimension of return value
If you need to write out a list in vertical orientation, the `transpose` option comes in handy:
```python
from xlwings import func, ret
@func
@ret(transpose=True)
def vertical_list():
return [1, 2, 3, 4]
```
and again with type hints:
```python
from typing import Annotated
from xlwings import func
@func
def vertical_list() -> Annotated[list, {"transpose": True}]:
return [1, 2, 3, 4]
```
## Error handling and error cells
When writing to Excel, error cells in Excel such as `#VALUE!` are used to display the Python error. When reading, xlwings turns error cells into `None` by default but optionally allows you to read them as strings. Let’s get into the details!
### Error handling
Whenever there’s an error in Python, the cell value will show `#VALUE!`. To understand what’s going on, click on the cell with the error, then hover (don’t click!) on the exclamation mark that appears: you’ll see the error message.
### Writing NaN values
`np.nan` and `pd.NA` will be converted to Excel’s `#NUM!` error type.
### Error cells
#### Reading error cells
By default, error cells such as `#VALUE!` are converted to `None` (scalars and lists) or `np.nan` (NumPy arrays and pandas DataFrames). If you’d like to get them in their string representation, use `err_to_str` option:
```python
from xlwings import func, arg
@func
@arg("x", err_to_str=True)
def myfunc(x):
...
```
and again with type hints:
```python
from typing import Annotated, Any
from xlwings import func
@func
def myfunc(x: Annotated[list[list[Any]], {"err_to_str"=True}):
...
```
#### Writing error cells
To format cells as proper error cells in Excel, simply use their string representation (`#DIV/0!`, `#N/A`, `#NAME?`, `#NULL!`, `#NUM!`, `#REF!`, `#VALUE!`):
```python
from xlwings import func
@func
def myfunc(x):
return ["#N/A", "#VALUE!"]
```
#### NOTE
Some older versions of Excel don’t support proper error types and will display the error as string instead.
## Dynamic arrays
If your return value is a one- or two-dimensional array such as a list, NumPy array, or pandas DataFrame, Excel will automatically spill the values into the surrounding cells by using the native dynamic arrays. There are no code changes required:
Returning a simple list:
```python
from xlwings import func
@func
def programming_languages():
return ["Python", "JavaScript"]
```
Returning a NumPy array with standard normally distributed random numbers:
```python
import numpy as np
from xlwings import func
@func
def standard_normal(rows, columns):
rng = np.random.default_rng()
return rng.standard_normal(size=(rows, columns))
```
Returning a pandas DataFrame:
```python
import pandas as pd
from xlwings import func
@func
def get_dataframe():
df = pd.DataFrame({"Language": ["Python", "JavaScript"], "Year": [1991, 1995]})
return df
```
## Volatile functions
Volatile functions are recalculated whenever Excel calculates something, even if none of the function arguments have changed. To mark a function as volatile, use the `volatile` argument in the `func` decorator:
```python
import datetime as dt
from xlwings import func
@func(volatile=True)
def last_calculated():
return f"Last calculated: {dt.datetime.now()}"
```
## Object handles
Object handles allow you to return Python objects such as a pandas DataFrame to a single cell. Other custom functions can then use the cell with the object handle as a function argument for further manipulation. This functionality is especially helpful if you have huge amounts of data or if the object can’t be “translated” into Excel cells.
#### NOTE
Object handles require xlwings >= 0.36.5, make sure to update `requirements.txt` if you are using an older version.

To make a custom function return an object handle, annotate the return value with `object`:
```python
from typing import Annotated
import pandas as pd
from xlwings import func, ret, ObjectHandle
from xlwings.constants import ObjectHandleIcons
@func
async def get_mymodel() -> object:
return pd.DataFrame(
{"A": [1, 2, 3, 4, 5], "B": [10, 8, 6, 4, 2], "C": [10, 9, 8, 7, 6]}
)
```
By default, this will display an icon in the cell together with the data type of the object (cell `A1` in the screenshot). By clicking on the icon, you will get some info about that object. You can, however, add valuable information by specifying a different `text`, `icon`, and `properties` (the fields shown on the object handle’s card, cell `A3` in the screenshot). There are three ways to do this:
- via the `ret` decorator
- via an annotated type hint
- by wrapping the return value in an `ObjectHandle`, which additionally lets you customize them per object
Using the `ret` decorator:
```python
@func
@ret(
icon=ObjectHandleIcons.table,
text="My Model",
properties={"Source": {"type": "String", "basicValue": "Model A"}},
)
async def get_mymodel() -> object:
return pd.DataFrame(
{"A": [1, 2, 3, 4, 5], "B": [10, 8, 6, 4, 2], "C": [10, 9, 8, 7, 6]}
)
```
The `properties` follow the [Excel entity property](https://learn.microsoft.com/office/dev/add-ins/excel/excel-data-types-entity-card) format and, when provided, replace the automatically derived ones (such as the type and shape). To do the same via annotated type hint, you would do:
```python
MyModel = Annotated[
object,
{
"icon": ObjectHandleIcons.table,
"text": "My Model",
"properties": {"Source": {"type": "String", "basicValue": "Model A"}},
},
]
@func
async def get_mymodel() -> MyModel:
return pd.DataFrame(
{"A": [1, 2, 3, 4, 5], "B": [10, 8, 6, 4, 2], "C": [10, 9, 8, 7, 6]}
)
```
If you instead want to customize them *per object*—for example, to show a text that depends on the value—wrap the return value in an `ObjectHandle`:
```python
from xlwings import func, ObjectHandle
from xlwings.constants import ObjectHandleIcons
@func
async def get_mymodel() -> object:
df = load_model_data()
if df.empty:
return ObjectHandle(df, text="No data", icon=ObjectHandleIcons.warning)
else:
return ObjectHandle(
df,
text=f"{len(df)} rows",
icon=ObjectHandleIcons.table,
)
```
`ObjectHandle` accepts the wrapped object as the first argument, followed by the same optional `text`, `icon`, and `properties` keyword arguments. Values set via `ObjectHandle` take precedence over those set via `ret` or the annotated type hint.
To be able to use an object handle as argument in another function, annotate the argument with `CachedObject[...]`, where `...` is the type of the wrapped object:
```python
import pandas as pd
from xlwings import func, CachedObject
@func
async def df_query(df: CachedObject[pd.DataFrame], query: str):
return df.query(query) # df is a DataFrame as far as your editor is concerned
```
The subscript keeps the static type information inside the function, so editors and type checkers know that `df` is, for example, a `pd.DataFrame` (and offer autocomplete for `df.query`). If the function should accept any object handle, use a bare `CachedObject`:
```python
from xlwings import func, CachedObject
@func
async def view(obj: CachedObject):
return obj
```
#### NOTE
You can also annotate the argument with `object` instead of `CachedObject[...]`. This works identically but loses the static type information inside the function.
If you are looking for functionality similar to how the `xl()` function works in Microsoft’s Python in Excel, you can do it as follows:
```python
from xlwings import arg, func
@func
@arg("df", index=False)
async def to_df(df: pd.DataFrame) -> object:
return df
```
This turns an existing Excel range into a DataFrame. Using an Excel table as your source range is a good idea as it makes your object handle dynamically update whenever you resize the Excel table.
## Plots
Custom functions can return Matplotlib figures as images. Because this relies on the xlwings image cache in the cloud, it requires an internet connection (during calculation only). It is enabled by default—if you’d rather not have images uploaded, turn off **Allow custom functions to return images** under **xlwings Lite menu > Settings > Local**, and functions returning a figure will show an error in the cell instead. Only the image is uploaded, no raw data or code. After the function calculation completes, the image is embedded in the Excel workbook and deleted from the cache.
#### NOTE
[Self-Hosting](self-hosting.md) uses an internal image cache, so there’s no cloud involved.
Consider the following example, which returns a Matplotlib figure of a sine wave:
```python
import numpy as np
import matplotlib.pyplot as plt
from xlwings import func
@func
def plot_sine(frequency=1):
x = np.linspace(0, 2 * np.pi, 500)
y = np.sin(frequency * x)
fig, ax = plt.subplots()
ax.plot(x, y, color="green", linewidth=2)
ax.fill_between(x, y, alpha=0.2, color="green")
return fig
```
Calling `=PLOT_SINE()` in cell A2 returns the plot as an in-cell image:

To get the big plot as shown in the screenshot, right-click on cell A2, then select **Picture in Cell > Create Reference**:

With a big plot, this feels similar to a native Excel chart (change the value in cell A1 from `1` to `2` to see the plot automatically update). There are several libraries that use Matplotlib under the hood:
- pandas
- Seaborn
- Plotnine
- etc.
You can use all of them too, just make sure to return the underlying Matplotlib figure. Here’s a pandas example:
```python
import pandas as pd
from xlwings import func
@func
def plot_df():
df = pd.DataFrame(
{"A": [1, 3, 2, 4, 3], "B": [2, 4, 3, 5, 6]}
)
ax = df.plot()
return ax.get_figure()
```
When opening a workbook with custom function plots, you will need to confirm the following security prompt:

If you don’t click **Enable Content**, a recalculation of the custom function will result in a `#BLOCKED!` error.
For more on plotting across custom functions, scripts, and the notebook, see [Plotting](plotting.md).
#### NOTE
Plots require xlwings>=0.36.8 in `requirements.txt`.
## Streaming functions (“RTD functions”)
In the traditional version of Excel, streaming functions were called “RTD functions” or “RealTimeData functions”. However, unlike traditional RTD functions, streaming functions don’t use a local COM server. Instead, the process runs as a background task and pushes updates to Excel.
To create a streaming function, you simply need to write an asynchronous generator. That is, you need to use `async def` instead of `def` and `yield` instead of `return`:
```python
import asyncio
import datetime
from xlwings import func
@func
async def streaming_clock():
while True:
yield f"{datetime.datetime.now():%H:%M:%S}"
await asyncio.sleep(1)
```
Streaming functions also support 1d or 2d return values:
```python
import asyncio
import numpy as np
from xlwings import func
@func
async def streaming_random(rows=3, cols=4):
rng = np.random.default_rng()
while True:
yield rng.standard_normal(size=(rows, cols))
await asyncio.sleep(1)
```
For a more practical example, here’s how you can stream the BTC price from a REST API (make sure to add `httpx` to your `requirements.txt`):
```python
# Requires httpx in your requirements.txt
import asyncio
import httpx
from xlwings import func
@func
async def btc_price():
async with httpx.AsyncClient() as client:
while True:
response = await client.get(
"https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT"
)
response_data = response.json()
yield float(response_data["price"])
await asyncio.sleep(1)
```
Note that streaming functions run in the async world, so you need to use async libraries for I/O operations. For example, instead of using `requests`, use an async library such as `httpx` or `aiohttp`.
#### NOTE
Streaming functions require xlwings>=0.36.1 in `requirements.txt`.
## Accessing the calling cell
If your custom function needs to know which cell it was called from, add an argument with the `xw.Caller` type hint:
```python
import xlwings as xw
from xlwings import func
@func
def get_caller(caller: xw.Caller):
return caller.address
```
`Caller` provides the following attributes:
| Attribute | Example | Description |
|--------------|--------------|------------------------------------|
| `address` | `B21` | A1 notation of the calling cell |
| `row` | `21` | 1-based row of the calling cell |
| `column` | `2` | 1-based column of the calling cell |
| `sheet_name` | `Sheet1` | Name of the sheet |
| `book_name` | `Book1.xlsx` | Name of the workbook |
`Caller` describes *where* the function was called from: it isn’t an `xlwings.Range`
and doesn’t give you access to the cell’s values etc. A custom function only ever sees its
own arguments—it can’t read a cell that isn’t passed in as an argument, and it can’t
write anywhere other than its own result range. To interact with the Excel object model
after a custom function returns, use [`WithScript`]().
#### NOTE
- [Streaming functions]() don’t report the calling cell, so using the `xw.Caller` type hint raises an error.
- The `xw.Caller` type hint requires xlwings>=0.36.17.
## Running a script after a custom function
Custom functions can only write their return value—either into the calling cell or, for [dynamic arrays](), spilled into the surrounding cells. They can’t write anywhere else in the workbook. If you need such a side effect, return `WithScript()` to have a [script](custom-scripts.md) run after the function returns:
```python
import xlwings as xw
from xlwings import WithScript, func, script
@script
def hello_args(book: xw.Book, name: str, number: int):
book.sheets[0]["A1"].value = f"{name} {number}"
@func
def hello_with_script(name):
return WithScript(
f"Hello {name}!",
hello_args,
args=[name, 42],
)
```
The first argument is the value written to the cell. The second argument is the script function. `args` are handed to the script and must be JSON-serializable.
If the script lives in a different file, import it:
```python
import scripts
from xlwings import WithScript, func
@func
def hello_with_script(name):
return WithScript(f"Hello {name}!", scripts.hello_args, args=[name, 42])
```
Combined with [`Caller`](), this lets you format the result range after a custom function returns:
```python
import pandas as pd
import xlwings as xw
from xlwings import Caller, WithScript, func, script
@script
def format_table(
book: xw.Book,
sheet_name: str,
address: str,
nrows: int,
ncols: int,
):
"""Formats the range that the custom function spilled into."""
sheet = book.sheets[sheet_name]
table = sheet[address].resize(nrows, ncols)
# Header row
header = table[0, :]
header.color = "#15a3a3"
header.font.color = "#ffffff"
header.font.bold = True
# Body: number format on the numeric columns
table[1:, 1:].number_format = "#,##0.00"
@func
def sales_report(caller: Caller):
df = pd.DataFrame({
"Product": ["Apples", "Bananas", "Cherries"],
"Q1": [1000.5, 2300.75, 1750.25],
"Q2": [1200.0, 2100.5, 1980.0],
})
df = df.set_index("Product")
# +1 row/col for the header row and the index column
nrows, ncols = df.shape[0] + 1, df.shape[1] + 1
return WithScript(
df,
format_table,
args=[caller.sheet_name, caller.address, nrows, ncols],
)
```
Limitations:
- The script runs once per successful call of the custom function. If you fill the formula down 500 rows, the script runs 500 times. Also make sure that the script doesn’t write to cells that the custom function depends on, as this would cause an endless loop.
- Streaming functions are not supported.
- The script is run at the next calculation boundary. This requires ExcelApi 1.8; on older versions, it is run on a best-effort basis right after the custom function returns.
#### NOTE
`xw.WithScript` requires xlwings>=0.36.17.
## Asynchronous functions
Custom functions are always asynchronous, meaning that the cell will show `#BUSY!` during calculation, allowing you to continue using Excel: custom functions don’t block Excel’s user interface.
## Custom functions vs. classic UDFs
While xlwings Lite custom functions are mostly compatible with the VBA-based UDFs from classic xlwings, there are a few differences, which you should be aware of when switching from UDFs to custom functions or vice versa:
| | Custom functions (xlwings Lite) | User-defined functions UDFs (classic xlwings) |
|---------------------------------------------------------|---------------------------------------------------------------------------------------|----------------------------------------------------------|
| Supported platforms | - Windows - macOS - Excel on the web | - Windows |
| Empty cells are converted to | `0` => If you want `None`, you have to set the following formula in Excel: `=""` | `None` |
| Cells with integers are converted to | Integers | Floats |
| Reading Date/Time-formatted cells | Requires the use of `dt.datetime` or `parse_dates` in the arg decorators | Automatic conversion |
| Writing datetime objects | Automatic cell formatting | No cell formatting |
| Can write proper Excel cell error | Yes | No |
| Writing `NaN` (`np.nan` or `pd.NA`) arrives in Excel as | `#NUM!` | Empty cell |
| Asynchronous functions | Always and automatically | Requires `@xw.func(async_mode="threading")` |
| Formula Intellisense | Yes | No |
| Supports namespaces e.g., `NAMESPACE.FUNCTION` | Yes | No |
| Capitalization of function name | Excel formula gets automatically capitalized | Excel formula has same capitalization as Python function |
| `caller` function argument | N/A | Returns Range object of calling cell |
| `@xw.arg(vba=...)` | N/A | Allows to access Excel VBA objects |
| Can return pictures | Matplotlib figures only (see [Plotting](plotting.md)) | Yes |
| Requires a local installation of Python | No | Yes |
## Limitations
- You can’t define the same function name with xlwings Lite as you define elsewhere, e.g., in VBA or Lambda functions.
- Custom Functions were introduced in 2018 and therefore require at least Excel 2021 or Excel 365.
- Note that some functionality requires specific build versions, such as error cells and date formatting, but if your version of Excel doesn’t support these features, xlwings will fall back to either string-formatted error messages or unformatted date serials.
# custom-scripts.md
# Scripts
Scripts are the equivalent to a `Sub` in VBA or an Office Script. They run at the click of a button and have access to the Excel object model, i.e., they can insert a new sheet, format an Excel range as a table, set the color of a cell, etc.
## Basic syntax
A script is a Python function that:
- has the `@script` decorator
- has a function argument with the `xlwings.Book` type hint
Here is how this looks:
```python
import xlwings as xw
from xlwings import script
@script
def hello_world(book: xw.Book):
sheet = book.sheets[0]
sheet["A1"].value = "Hello xlwings!"
```
The `book` argument represents the active workbook and can be called differently if you like. E.g., if you want to call the argument `wb` instead of `book`, you would write `wb: xw.Book` instead of `book: xw.Book`.
You can configure scripts, see [Script Configuration]() below. If your scripts are slow, see [Performance]().
## Script arguments
Scripts can accept additional arguments after the `book` argument. Add a type hint to each one, and App Mode renders a matching [form element](app-mode.md#form-fields) above the script’s button:
```python
import datetime as dt
from typing import Literal
import xlwings as xw
from xlwings import script
@script(name="Create Report")
def create_report(
book: xw.Book,
title: str,
start: dt.date,
rows: int = 10,
tax_rate: float = 0.081,
currency: Literal["USD", "EUR", "CHF"] = "USD",
include_totals: bool = False,
):
"""# Monthly Report"""
sheet = book.sheets[0]
sheet["A1"].value = title
sheet["B1"].value = start # already a datetime.date
amounts = [[i, i * (1 + tax_rate)] for i in range(1, rows + 1)]
sheet["A3"].value = amounts
if include_totals:
sheet[f"A{3 + rows}"].value = ["Total", sum(row[1] for row in amounts)]
sheet[f"B3:B{3 + rows}"].number_format = f'#,##0.00 "{currency}"'
```
In App Mode, this looks like this:

The type hint determines which form element is used:
| Type hint | Form element | Value your script receives |
|----------------------|----------------------|------------------------------|
| `str` | Text box | `str` |
| `int` | Number box | `int` |
| `float` | Number box | `float` |
| `bool` | Checkbox | `bool` |
| `Literal["a", "b"]` | Dropdown | The selected value |
| `datetime.date` | Date picker | `datetime.date` |
| `datetime.datetime` | Date and time picker | `datetime.datetime` |
| Anything else / none | Text box | `str` |
#### NOTE
Date conversion requires xlwings >= 0.36.11, make sure to update `requirements.txt` if you are using an older version. On older versions, `datetime.date` and `datetime.datetime` arguments arrive as ISO-formatted strings, which you can convert with `dt.date.fromisoformat()` or `dt.datetime.fromisoformat()`.
### Labels and help text
By default, the form labels each field with the parameter name. To show something friendlier, wrap the type hint in `typing.Annotated` with a `label` and an optional `description`:
```python
from typing import Annotated
@script(name="Create Report")
def create_report(
book: xw.Book,
n_rows: Annotated[int, {"label": "Number of rows"}] = 10,
tax_rate: Annotated[float, {"label": "Tax rate", "description": "As a decimal, e.g. 0.081"}] = 0.081,
):
...
```
Here’s how `tax_rate` ends up looking:

The `label` replaces the parameter name in the form and in any validation message, and the `description` is shown as muted help text between the label and the field, so it’s read before the value is entered. Everything else — which widget is used, defaults, whether the argument is required — still comes from the type hint and the signature, so you can add a label to any argument without changing its behavior.
Scripts with arguments are only runnable in App Mode except if they all have a default argument.
## Button Label
By default, the button shows the name of the function. To change this, provide a `name` argument in the `@script` decorator:
```python
import xlwings as xw
from xlwings import script
@script(name="Run Hello World")
def hello_world(book: xw.Book):
sheet = book.sheets[0]
sheet["A1"].value = "Hello xlwings!"
```
## Running a script from the add-in
To run a script, click the play button in the editor gutter next to its function, click the run button, or press `F5`:

To select a different script to run, select it via dropdown:

Whenever you add a new script or change the name of an existing script, the button and dropdown will update automatically.
The dropdown lists scripts from `main.py` first, followed by other workbook modules alphabetically and then personal modules alphabetically. Within each workbook module, scripts retain their source-code order.
## Running a script via sheet button
Office.js (the add-in technology that xlwings Lite is programmed in) doesn’t offer a native way to connect a button on a sheet to a script. Therefore, xlwings Lite supports a solution via shapes and hyperlinks. While this is a workaround, it offers a user experience that is on par with the official buttons that Office Scripts offers.
#### WARNING
If your script depends on the selected cells, this solution currently doesn’t work as clicking the button will change the selected cell. Run your script from the task pane instead.
1. On the Excel ribbon, go to `Insert` > `Shapes` and select e.g., a rounded rectangle. Then draw the shape on the sheet. Adjust colors and text to your liking.
2. Select the shape. In the name box (on the left of the formula bar), give it a specific name, then hit Enter:

For the rest of this example we’re going to use `xlwings_button`.
3. Right-click on the shape and select `Link` (Windows) or `Hyperlink...` (macOS). On the tab `Place in This Document` (Windows) or `This Document` (macOS), where it says `Type the cell reference`, write the name of a cell that is hidden behind the shape, e.g., `B4`. Confirm by clicking on `OK`.
4. Provide the following arguments in the `script` decorator (`show_taskpane` is optional and opens the task pane if it closed):
```python
@script(button="[xlwings_button]Sheet1!B4", show_taskpane=True)
def hello_world(book: xw.Book):
...
```
How does it work? When the xlwings Lite saves the Python code, it registers an event handler that runs the script when the referenced cell is selected. This happens when you click the button as we have set up a hyperlink. Immediately after that cell has been selected, it selects the cell below it to be ready for the next call.
#### NOTE
When you open a workbook with a sheet button, you currently need to first open xlwings Lite to activate the buttons. In a future release, you will be able to set the add-in to start automatically so this won’t be required anymore.
### Sheet button troubleshooting
- Make sure that you haven’t initially selected the cell that is referenced under `button`.
- Make sure the `button` reference in the script decorator is correct, especially with regard to the button name: `button=[button_name]Sheet1!A1`.
- Restart xlwings Lite to make sure that event handlers are properly registered.
#### NOTE
Excel on the web doesn’t allow you to add a hyperlink to a shape. However, workbooks that were set up on the desktop version of Excel also work with Excel on the web.
## Script Configuration
To configure scripts, you can provide the decorator with arguments, e.g.:
```python
import xlwings as xw
from xlwings import script
@script(include=["Sheet1", "Sheet2"])
def hello_world(book: xw.Book):
sheet = book.sheets[0]
sheet["A1"].value = "Hello xlwings!"
```
Here are the settings that you can provide:
- `exclude` (optional): By default, xlwings sends over the content of the whole workbook to Python. If you have sheets with big amounts of data, this can make the calls slow or timeout. If your code doesn’t need the content of certain sheets, the exclude option will block the sheet’s content (e.g., values, pictures, etc.) from being sent to Python. Currently, you can only exclude entire sheets like so: `exclude=["Sheet1", "Sheet2"]`.
- `include` (optional): It’s the counterpart to exclude and allows you to submit the names of a few seleceted sheets whose content (e.g., values, pictures, etc.) you want to send to Python. Currently, you can only include entire sheets like so: `include=["Sheet1", "Sheet2"]`.
- `button` (optional): If you want to use a sheet button, you need to provide the reference for the button and its linked cell, e.g., `button=[mybutton]Sheet1!A1`.
- `show_taskpane` (optional): Use this in connection with `button`. If `show_taskpane=True`, the task pane will automatically show up when the user clicks on a sheet button.
## Performance
By default, xlwings Lite transfers the content of the whole workbook to Python up front. For small workbooks, you won’t notice this. But if you have sheets with large amounts of data, this transfer can make your scripts slow or even cause them to time out.
There are two ways to deal with this:
### 1. Limit the data with `include`/`exclude`
If your script only needs a few sheets, use the [`include` or `exclude`]() arguments in the `@script` decorator to restrict which sheets are sent to Python:
```python
import xlwings as xw
from xlwings import script
# Only send Sheet1 and Sheet2 to Python
@script(include=["Sheet1", "Sheet2"])
def include_sample(book: xw.Book):
sheet = book.sheets[0]
sheet["A1"].value = "Hello xlwings!"
# Send everything except the data-heavy sheets
@script(exclude=["BigData1", "BigData2"])
def exlude_sample(book: xw.Book):
sheet = book.sheets[0]
sheet["A1"].value = "Hello xlwings!"
```
Note that `include` and `exclude` operate on entire sheets: an excluded sheet’s content isn’t available to your script at all. If you need finer-grained control, use the async API described next.
### 2. Read on demand with the async API
`include`/`exclude` still transfer the full content of the sheets they let through. For an even more efficient approach that transfers only the exact values you request—working the same way as classic, locally installed xlwings—use the [Async API](async-api.md). Instead of loading the whole workbook up front, it reads and writes cell values on demand, which avoids the up-front transfer entirely.
## Excel object model
To learn about the Excel object model, have a look at the following docs from xlwings:
- [API reference](https://docs.xlwings.org/en/latest/api/index.html) (see also [Limitations]())
It’s also worth looking at the following tutorials:
- [Syntax Overview](https://docs.xlwings.org/en/latest/syntax_overview.html)
- [Data Structures](https://docs.xlwings.org/en/latest/datastructures.html)
- [Matplotlib](https://docs.xlwings.org/en/latest/matplotlib.html)
- [Converters](https://docs.xlwings.org/en/latest/converters.html)
## Limitations
- Script arguments must be JSON-serializable, and keyword-only arguments (after a `*`) as well as `**kwargs` aren’t supported, see [Script arguments]().
- xlwings Lite doesn’t support the `api` property that classic xlwings offers to workaround missing features.
- At the moment, xlwings Lite doesn’t cover yet 100% of the xlwings API. The following attributes are currently missing:
```text
xlwings.App
- cut_copy_mode
- quit()
- display_alerts
- startup_path
- calculate()
- status_bar
- path
- version
- screen_updating
- interactive
- enable_events
- calculation
xlwings.Book
- to_pdf()
- save()
xlwings.Characters
- font
- text
xlwings.Chart
- set_source_data()
- to_pdf()
- parent
- delete()
- top
- width
- height
- name
- to_png()
- left
- chart_type
xlwings.Charts
- add()
xlwings.Font (setting the following properties is supported, only getting them isn't!)
- size
- italic
- color
- name
- bold
xlwings.Note
- delete()
- text
xlwings.PageSetup
- print_area
xlwings.Picture
- top
- left
- lock_aspect_ratio
xlwings.Range
- hyperlink
- formula
- font
- width
- formula2
- characters
- to_png()
- columns
- height
- formula_array
- paste()
- rows
- note
- merge_cells
- row_height
- get_address()
- merge()
- to_pdf()
- autofill()
- top
- wrap_text
- merge_area
- column_width
- copy_picture()
- table
- unmerge()
- current_region
- left
xlwings.Shape
- parent
- delete()
- font
- top
- scale_height()
- activate()
- width
- index
- text
- height
- characters
- name
- type
- scale_width()
- left
xlwings.Sheet
- page_setup
- used_range
- shapes
- charts
- autofit()
- copy()
- to_html()
- select()
- visible
xlwings.Table
- display_name
- show_table_style_last_column
- show_table_style_column_stripes
- insert_row_range
- show_table_style_first_column
- show_table_style_row_stripes
```
# databases.md
# Databases
Direct connections with traditional SQL databases aren’t supported. Here’s what you can do about it:
- Put a web API in front of your database. You can either build something custom or use an existing solution:
- For PostgreSQL, there’s [PostgREST](https://docs.postgrest.org). For a hosted solution, see [Supabase]() below.
- For Oracle, there is [Oracle REST Data Services (ORDS)](https://www.oracle.com/database/technologies/appdev/rest.html).
- For MySQL, there is [MySQL REST Service (MRS)](https://dev.mysql.com/doc/dev/mysql-rest-service/latest).
- Use an SQLite database that you download from a network location. Note that this requires to add `sqlite3` to `requirements.txt`.
A few database solutions come with a native REST API:
- [NocoDB](https://nocodb.com)
- [InfluxDB](https://www.influxdata.com)
- [CouchDB](https://couchdb.apache.org)
## Supabase
[Supabase](https://supabase.com/) is a hosted Postgres database, which includes PostgREST. They offer a free plan and since the solution is open-source, you could also self-host it.
While Supabase offers Python packages like `supabase` and `postgrest`, they currently have a few issues when used with xlwings Lite. While these issues will likely be resolved in the future, for now, you can use the PostgREST interface directly via either `requests` or `aiohttp` (`httpx` is currently having an issue).
#### ATTENTION
If you use `aiohttp`, make sure to add both `aiohttp` and `ssl` to `requirements.txt`.
Here is the sample code:
```python
import requests
import aiohttp
import xlwings as xw
from xlwings import func, script
@script
def db_requests(book: xw.Book):
key = "" # When supported, should be handled via env var
url = "https://.supabase.co/rest/v1/"
headers = {
"apikey": key,
"Authorization": f"Bearer {key}",
}
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
print(data)
else:
print(f"Error: {response.status_code}")
except Exception as e:
print(f"Unexpected error: {e}")
@script
async def db_aiohttp(book: xw.Book):
key = "" # When supported, should be handled via env var
url = "https://.supabase.co/rest/v1/"
headers = {
"apikey": key,
"Authorization": f"Bearer {key}",
}
async with aiohttp.ClientSession() as session:
try:
async with session.get(url, headers=headers) as response:
if response.status == 200:
data = await response.json()
print(data)
else:
print(f"Error: {response.status}")
except Exception as e:
print(f"Unexpected error: {e}")
```
For example, for `` you can use a table name to return all records of that table. For details about the query syntax, see [PostgREST API](https://docs.postgrest.org/en/stable/references/api.html).
# dependencies.md
# Dependencies
This document describes how to install, uninstall, and update Python packages. Since xlwings Lite runs Python via [Pyodide](https://pyodide.org), a Python distribution for WebAssembly (Wasm), there are certain packages that can’t be installed, as explained under [Compatible packages](). To keep package versions stable across restarts, Pyodide 314.0.0+ is required.
## Installing and uninstalling packages
To define your dependencies, open `requirements.txt` and add or remove your Python packages there. Whenever you edit the file, a notice appears telling you to restart xlwings Lite to apply the change:

Since xlwings Lite records the exact versions it installs in `requirements-pinned.txt`, you usually don’t want to pin an exact version in `requirements.txt`. What you might want to do is to specify an upper bound, e.g.,
```text
xlwings<1.0.0
```
## requirements-pinned.txt
xlwings Lite records the exact versions of all packages installed from `requirements.txt` in the auto-generated file `requirements-pinned.txt`:
```text
# Generated by xlwings Lite
# source sha256: 7f3a...
numpy==2.0.2
pandas==2.2.3
```
On every subsequent restart, those versions are reinstalled. Your workbook therefore keeps working with the same package versions it was built and tested against, even if you never pinned anything yourself. The file includes your top-level dependencies from `requirements.txt` as well as dependencies of your dependencies (“sub-dependencies”).
`requirements-pinned.txt` records package names and versions, but not wheel URLs or hashes. It keeps versions stable; it isn’t a complete lockfile that identifies the exact wheel artifacts.
You normally don’t edit `requirements-pinned.txt` directly. Instead, changes to `requirements.txt` are reflected in `requirements-pinned.txt` automatically after a restart.
#### ATTENTION
**Pyodide 0.27.5** can’t enforce the versions in `requirements-pinned.txt`. With 0.27.5, dependencies are resolved from `requirements.txt` and `requirements-pinned.txt` is refreshed after every startup so it serves as an informational record only, showing the packages that are currently installed. We therefore recommend upgrading to Pyodide 314.0.0+ by going to **Settings** > **Workbook** > **Pyodide Version**. Before doing so, make a backup copy of your workbook: the upgrade may change dependency versions, including major versions such as pandas, which is updated from v2 to v3.
If you need to stick to Pyodide 0.27.5, you can manually copy the content from `requirements-pinned.txt` and paste it into `requirements.txt` as a workaround.
## Updating packages
Open either the `requirements.txt` or `requirements-pinned.txt` tab and use the following buttons:
- **Update all**: re-resolves everything from `requirements.txt` and updates `requirements-pinned.txt` accordingly.
- **Update xlwings**: updates only xlwings while keeping all other packages at their recorded versions.
Both buttons will restart xlwings Lite.
#### NOTE
These actions update versions recorded in `requirements-pinned.txt`. They never change or override exact pins written in `requirements.txt`. Edit `requirements.txt` directly to change an exact pin.
Changes are reported in the Output pane after the restart so you always see which versions moved. For example, you’d see the following output if pandas is upgraded from v2.2.3 to 3.0.2:
```diff
Updated requirements-pinned.txt
- pandas==2.2.3
+ pandas==3.0.2
```
#### ATTENTION
Both buttons require Pyodide 314.0.0+. On Pyodide 0.27.5, versions are re-resolved from `requirements.txt` on every startup anyway, so neither action applies.
If a version recorded in `requirements-pinned.txt` isn’t available—or conflicts with a specifier you added to `requirements.txt`—xlwings Lite tells you so and keeps your file rather than silently replacing it. A notice offers **Try to fix dependency version conflicts**, which resolves fresh versions from `requirements.txt` and records the result.
## Compatible packages
Pyodide first checks [PyPI](https://pypi.org/) for a compatible package in the wheel (`.whl`) format. Compatible wheels are either
- Pure Python packages
- Packages built for Pyodide’s WebAssembly platform, i.e. wheels tagged `pyodide__wasm32` (only for Pyodide 314.0.0+)
If it doesn’t find a compatible wheel on PyPI, Pyodide checks their own repository where they host compatible wheels for many popular packages that aren’t pure Python.
If there’s no compatible version of the package, you could build it yourself, see [Creating a Pyodide package](https://pyodide.org/en/stable/development/new-packages.html). Note, however, that there are a few packages that are hard to build and therefore currently aren’t available for Pyodide, e.g., PyTorch.
## Web locations
If packages are on a web server, you can reference them with their URL in the `requirements.txt` file:
```text
https://myserver.com/mypackage-x.x.x-py3-none-any.whl
```
The server must allow cross-origin requests (CORS), and the last component of the URL must be a valid wheel filename. Keep such URLs in `requirements.txt`: that line is what locates the wheel. The package still appears by name and version in `requirements-pinned.txt`, but it is the URL that installs it.
## File menu
You can drop your package under **Files** > **Import/Export**, then put it on `requirements.txt` under the name `/data/my-package.whl`. Adjust the path accordingly if you use **Local Folders** instead of **Import/Export**.
# editor.md
# Editor
xlwings Lite uses the same editor as VS Code. This means that many of its familiar features are available.
## AutoSave
When you’re writing Python code or editing the `requirements.txt` file, changes are automatically written to the workbook. The active tab shows this with a green checkmark. This makes sure that when you run a script or execute custom functions, you’re always using the latest version of the code as shown in the editor.
If you are storing the working on OneDrive/SharePoint with `AutoSave` enabled, everything is automatically and continuously saved to the workbook.
However, if you have `AutoSave` disabled, changes to the Python code and the `requirements.txt` file are only saved when you save the workbook.
## Keyboard shortcuts
Many of the default VS Code keyboard shortcuts are available. A few examples:
| Action | Windows/Linux | macOS |
|----------------------------------------------------------------|--------------------------|-----------------------|
| Move a line up or down | `Alt` + `↑`/`↓` | `Alt` + `↑`/`↓` |
| Delete an entire line | `Shift` + `Ctrl` + `K` | `Shift` + `⌘` + `K` |
| Insert multi-cursor above/below | `Alt` + `Ctrl` + `↑`/`↓` | `Alt` + `⌘` + `↑`/`↓` |
| Lint and format code with [Ruff](https://docs.astral.sh/ruff/) | `Shift` + `Alt` + `F` | `Shift` + `Alt` + `F` |
| Run selected script | `F5` | `F5` |
| Increase/decrease editor font size | `Ctrl` + `+`/`-` | `⌘` + `+`/`-` |
## Auto Completion
Autocompletion works similarly to VS Code:

## Output pane
The output pane at the bottom of the editor can be vertically resized:

Anything that you `print()` will show up in the output pane.
## Standalone editor
For that extra VBA feeling, you can drag the xlwings Lite add-in out of Excel by grabbing it in the gray title bar and pulling it out:

# environment-variables.md
# Environment Variables
xlwings Lite runs in the secure sandbox of a browser and therefore can’t access anything on your local computer. This means that it also can’t access local environment variables such as `USERPROFILE`, etc.
However, you can set environment variables with two different scopes:
- **Local scope**: stored in the browser’s local storage and available across all workbooks. However, they will be deleted when you clear the Office cache, so make a backup!
#### WARNING
**Security Warning:** When using environment variables with scope “Local”, only open trusted workbooks from known sources. Malicious workbooks could otherwise exfiltrate these variables.
- **Workbook scope**: stored in the current workbook.
**Note:** Local scope overrides Workbook scope.
To set an environment variable:
- Provide the `Name`, `Value`, and `Scope` and hit `Save`.
- Restart xlwings Lite
For example, if you set the following environment variable:
- Name: `OPENAI_API_KEY`
- Value: `your-key`
You can access them like this in your code:
```python
import os
import xlwings as xw
from xlwings import func, script
@script
def sample_script(book: xw.Book):
key = os.getenv("OPENAI_API_KEY")
if key is not None:
print(key)
else:
raise Exception("Store your OPENAI_API_KEY key under Environment Variables!")
```
# faq.md
# FAQ
## Installation & Runtime
### The add-in store is blocked in my company. What can I do?
The easiest solution is to ask your Office admin to install xlwings Lite via Office 365 Admin Center. If they are unwilling to do so, let them know that xlwings Lite can also be [self-hosted](self-hosting.md).
### Does xlwings Lite require Python to be installed locally?
No, installing the add-in is the only requirement.
### Is there any user or usage quota or limitation?
No.
## Code, Packages & Data
### Where is the Python code stored?
The Python code is stored inside the workbook, making it easy to distribute your workbook.
### Can I install the Python packages I want?
Yes, with certain limitations, see [Dependencies](dependencies.md).
### Can I make web API calls?
Yes, as long as the server supports CORS, see [Web API Requests](web-requests.md).
## Privacy & Distribution
### Does xlwings Lite send out any data?
No, xlwings Lite runs entirely on the end-user’s machine and doesn’t send any data out. There is one exception: custom functions that return Matplotlib figures need to send the picture to the xlwings image cache. This can be turned off under **xlwings Lite menu > Settings > Local**, see [Custom functions that return images](security.md#custom-functions-that-return-images). For usage statistics, we use [Plausible](https://plausible.io/), which is fully GDPR, CCPA, and PECR compliant (this won’t be done for the self-hosted version).
### Can I protect my Python code?
No, your Python code is part of the workbook and therefore accessible to the end-user. If you need to protect your Python code, you can use [xlwings Server](https://server.xlwings.org).
### Can I build an Excel add-in with xlwings Lite?
No. To build an add-in, you need to either use [xlwings Server](https://server.xlwings.org/) or [xlwings Wasm](https://server.xlwings.org/en/latest/index_wasm/). xlwings Lite itself is built with xlwings Wasm.
## Comparisons
### What is the difference between xlwings Lite and Microsoft’s official Python in Excel solution?
See [xlwings Lite vs. Python in Excel](https://www.xlwings.org/lite/vs-python-in-excel).
### What is the difference between xlwings Lite and Anaconda Code?
See [xlwings Lite vs. Anaconda Code](https://www.xlwings.org/lite/vs-anaconda-code).
# files.md
# Files
xlwings Lite allows you to work with local files in two ways:
- **Import/Export**: store files inside the add-in via drag and drop (all platforms).
- **Local Folders**: mount folders from your computer (desktop Excel on Windows only).
#### IMPORTANT
For performance and security reasons, only share the files and folders that your code needs. Mounting or importing too many files can slow things down. Also, keep in mind that any code you run can access every file you share with the add-in.
## Import/Export
Go to **xlwings Lite menu** > **Files** > **Import/Export** to import and export files and folders. Files that you import are available in Python under `/data`, and any files that your Python code writes to `/data` show up here for download:

For example, to read an imported file called `input.csv`:
```python
import pandas as pd
df = pd.read_csv("/data/input.csv")
```
And to write a file that will then appear in the Import/Export list:
```python
df.to_csv("/data/output.csv")
```
You can drop files or entire folders onto the drop zone. If you upload folders, their paths are preserved, such as `/data/my/folder/`. Each file can be downloaded or deleted individually, and you can also **Download All** (as a ZIP file) or **Delete All**.
#### NOTE
Files will be deleted from xlwings Lite whenever the Office cache is cleared, so make sure to keep a backup.
## Local Folders
#### NOTE
Local Folders are only supported with desktop Excel on Windows. On other platforms, use [Import/Export]() instead.
Instead of importing individual files, you can mount a folder from your computer. Once mounted, your Python code can read and write files in that folder directly on your computer. A mounted folder is accessible across workbooks and survives restarts of xlwings Lite.
To mount a folder, go to **xlwings Lite menu** > **Files** > **Local Folders**:

Now click on **Add Folder**. Mounting a folder requires you to confirm two security pop-ups (if you can’t read the security pop-ups, make the xlwings Lite task pane bigger):

By default, a folder is mounted at a path based on its name, but you can rename the mount path by clicking the pencil icon. For example, if you mount a folder under `/xlwings_data`, you can access its files like this:
```python
import pandas as pd
df = pd.read_csv("/xlwings_data/input.csv")
df.to_csv("/xlwings_data/output.csv")
```
When a folder is mounted under `/data`, the [Import/Export]() tab is disabled to avoid conflicts.
You may need to re-authorize mounted folders after restarting xlwings Lite or occasionally thereafter. xlwings Lite will notify you when re-authorization is required:

Under **Files** > **Local Folders**, click on **Confirm** for the corresponding mount:

As with adding the mount, this will trigger two (in this case identical) prompts:

Make sure to select **Allow on every visit** and you shouldn’t have to re-authorize until the Office cache is cleared.
#### NOTE
As with imported files, the mount configuration is stored in the Office cache and will be lost whenever the cache is cleared, in which case you’ll have to re-mount the folder.
## File system layout
Your Python code sees two directories:
- `/data` is the **working directory** and the location that backs [Import/Export](). Relative paths therefore read and write under `/data`:
```python
df.to_csv("output.csv") # same as /data/output.csv
```
Files written this way survive a restart of xlwings Lite and show up under **Import/Export** for download (or, if you mounted a local folder at `/data`, they land directly in that folder).
- `/src` holds the **source files** from the editor’s file menu (e.g., `main.py` or a `config.json` you created there). To read one of them from code, use the following approach (works in scripts, modules, and notebooks):
```python
from pathlib import Path
config = (Path(__file__).parent / "config.json").read_text()
```
This resolves to `/src/config.json`, so the literal path works too.
#### NOTE
Unlike `/data`, source files are stored in the workbook itself, so they travel with it when you share it. They must be text files (e.g., `.py`, `.json`, `.csv`, `.md`) — binary files like Excel workbooks or images can’t be stored under `/src`.
# hosting-solutions.md
# Hosting Solutions
xlwings Lite runs on any platform capable of running a Docker image. This includes Linux VMs, Kubernetes clusters, and container hosting services such as Azure Container Apps, AWS ECS/Fargate, or Google Cloud Run. Whichever platform you choose, see [Configuration](configuration.md) for the available settings.
Here are specific instructions for certain providers/hosting solutions:
* [Linux VM](linux-vm.md)
* [Azure Container Apps](azure-container-apps.md)
* [AWS ECS Express Mode](aws-ecs-express.md)
# limitations.md
# Limitations
xlwings Lite is based on [Pyodide](https://pyodide.org), and comes with the following limitations:
- Packages that depend on the `multiprocessing` or `threading` modules aren’t currently supported.
- Some packages may need to be built specifically for Pyodide if they aren’t available yet and if they aren’t pure Python packages, see [Creating a Pyodide package](https://pyodide.org/en/stable/development/new-packages.html). For a list of “complex” packages that are available for Pyodide, see [Packages built in Pyodide](https://pyodide.org/en/stable/usage/packages-in-pyodide.html).
- Web APIs: Often, you’ll run into CORS issues as the server needs to explicitly allow access via headers. GitHub works though so you can access files from there. Otherwise, you could use a CORS proxy (preferably owned by you) to work around this issue if you have no control over the web server, see [Web API Requests](web-requests.md)
- TCP/IP connections: TCP/IP connections aren’t supported, which means that you can’t directly connect to databases like PostgreSQL. SQLite, however, is supported, see [Databases](databases.md).
- Pyodide currently has a memory limit of 2GB.
- There is currently no debugger.
See the [Pyodide docs](https://pyodide.org/en/stable/usage/wasm-constraints.html) for further details.
# linux-vm.md
# Linux VM
This guide shows how to self-host xlwings Lite on a plain Linux VM using the `xlwings-lite` CLI.
## Prerequisites
- A Linux VM (e.g., Ubuntu 24.04 LTS). Recommended minimal specs: 1 vCPU, 1 GB RAM, 25 GB SSD.
- A hostname pointing to the Linux VM, e.g., `xlwings-lite.mycompany.com`.
- Docker installed on the VM:
```shell
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh
```
If this doesn’t work, follow [Docker’s official installation guide](https://docs.docker.com/engine/install/).
- `bzip2` installed on the VM:
- Ubuntu/Debian: `sudo apt install -y bzip2`
- RHEL/Fedora/Amazon Linux: `sudo dnf install -y bzip2`
- An xlwings license key. If you don’t have one, you can request a [trial key](https://www.xlwings.org/trial).
## Install
### 1. Provide TLS certificates
xlwings Lite is an Office.js add-in, which requires TLS certificates even when hosted internally. xlwings Lite expects your cert and key under `/etc/xlwings-lite/certs/`, named `tls.crt` and `tls.key`.
If you need help with creating the certificates, see here:
- [TLS certificates via Windows CA Server]()
- [TLS certificates via Let's Encrypt (certbot)]()
### 2. Install the xlwings-lite CLI
```shell
sudo curl -fsSL https://lite.xlwings.org/cli -o /usr/local/bin/xlwings-lite
sudo chmod +x /usr/local/bin/xlwings-lite
```
### 3. Run the installer
```shell
sudo xlwings-lite install
```
Once `install` prints `Running`, visit `https://` in your browser to confirm (the page shows the version of xlwings Lite).
### 4. Register the add-in with Microsoft 365 admin center
1. In your browser, go to `https:///manifest`, which will download `xlwings-lite-manifest.xml`.
2. Go to [Microsoft 365 admin center](https://admin.microsoft.com/)
- Click on [`Show all` > `Settings` > `Integrated Apps`](https://admin.microsoft.com/#/Settings/IntegratedApps).
- If you have **xlwings Lite** installed, uninstall it first.
- Click on `Upload custom apps` and select `Office Add-in` (App type).
- Select `Upload manifest file (.xml) from device`. Click `Choose File`, then select the `xlwings-lite-manifest.xml` from the previous step.
- Click `Next`, then assign the desired users.
- Click `Next` and accept permission requests.
- Click `Next` and `Finish deployment`.
The users will get the add-in to show up automatically although it may take a few hours.
## Maintenance
### Update xlwings Lite
To update xlwings Lite to the latest version, run:
```shell
sudo xlwings-lite update
```
### Update the xlwings-lite CLI
To update the CLI, simply re-run the install command:
```shell
sudo curl -fsSL https://lite.xlwings.org/cli -o /usr/local/bin/xlwings-lite
sudo chmod +x /usr/local/bin/xlwings-lite
```
### Edit the configuration
The config file is `/etc/xlwings-lite/xlwings-lite.conf`. After editing, run `sudo xlwings-lite restart` to apply. See [Configuration](configuration.md) for the full list of supported settings.
### Download PyPI packages locally
By default, users install Python packages from [pypi.org](https://pypi.org/) at runtime by editing their `requirements.txt` file in xlwings Lite. If you want to restrict this but don’t want to set up a proper 3rd party registry, set `XLWINGS_ENABLE_PYPI=false` and download the PyPI packages with `xlwings-lite packages add`. You’ll have to download all packages your users depend on, but at the very least the following:
```text
sudo xlwings-lite packages add xlwings==0.35.3 seaborn==0.13.2
```
You can also pass a requirements file (this assumes it’s stored under `/etc/xlwings-lite`):
```text
sudo xlwings-lite packages add -r /etc/xlwings-lite/requirements.txt
```
#### NOTE
`packages add` only downloads pure-Python wheels and does not resolve sub-dependencies automatically. This is intentional: compiled packages like numpy, pandas, and matplotlib are already built into Pyodide. If a package has sub-dependencies, add them explicitly.
To disable direct PyPI access, set the environment variable:
```shell
sudo xlwings-lite config set XLWINGS_ENABLE_PYPI false
sudo xlwings-lite restart
```
With this configuration, users can only install packages that have been downloaded via `packages add` (or that are built into Pyodide).
Wheels and index files are stored under `/var/lib/xlwings-lite/pypi/`.
### Uninstall
```shell
sudo xlwings-lite stop
sudo rm /usr/local/bin/xlwings-lite
sudo rm -rf /etc/xlwings-lite /var/lib/xlwings-lite
```
You may also want to remove Docker and the Docker images.
#### NOTE
If you want to remove the add-in again and run into issues (“Remove apps failed. No apps were successfully removed. Please try to remove them later.”), use this legacy URL: [https://admin.microsoft.com/#/Settings/AddIns](https://admin.microsoft.com/#/Settings/AddIns)
### TLS certificates via Windows CA Server
If you issue your TLS certificates from a Windows CA Server (i.e., Active Directory Certificate Services / AD CS), here’s how to obtain one for your Linux VM. Steps marked *(Linux)* are run on the Linux VM (e.g., via PuTTY); steps marked *(Windows CA)* are run on the Windows CA server in a command prompt with admin rights.
1. *(Linux)* Change into the certs folder:
```shell
mkdir -p /etc/xlwings-lite/certs
cd /etc/xlwings-lite/certs
```
2. *(Linux)* Create a config file. Make sure to replace `` in both places with your own domain name:
```shell
cat > csr.cnf <<'EOF'
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
CN =
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 =
EOF
```
3. *(Linux)* Create the CSR file:
```shell
openssl req -new -key tls.key -out tls.csr -config csr.cnf
```
4. Copy `/etc/xlwings-lite/certs/tls.csr` from the Linux VM to the Windows CA server, e.g., using WinSCP.
5. *(Windows CA)* Generate the leaf certificate:
```shell
certreq -submit -attrib "CertificateTemplate:WebServer" tls.csr tls.leaf.crt
```
6. *(Windows CA)* Export the issuing CA certificate:
```shell
certutil -ca.cert ca.crt
```
This exports the certificate of the CA that signed your leaf cert, which you’ll need in the next step to complete the chain.
7. Copy `tls.leaf.crt` and `ca.crt` back to the Linux VM (e.g., using WinSCP). Copy them to `/tmp/` first to avoid any permission errors, then move them into place:
```shell
sudo mv /tmp/tls.leaf.crt /tmp/ca.crt /etc/xlwings-lite/certs
```
8. *(Linux)* Build the fullchain certificate that the app will actually serve:
```shell
cd /etc/xlwings-lite/certs
cat tls.leaf.crt ca.crt > tls.crt
```
#### NOTE
In a 2-tier (or deeper) AD CS setup, the CA that signed your leaf cert is an *intermediate* CA whose own certificate is signed by an offline *root* CA. In that case `ca.crt` (the intermediate) alone is not enough as clients need the full chain from leaf to root. Concatenate all certs in order from leaf to root:
```shell
cat tls.leaf.crt intermediate.crt root.crt > tls.crt
```
To check what’s currently in your chain, run `openssl crl2pkcs7 -nocrl -certfile tls.crt | openssl pkcs7 -print_certs -noout` and verify each `subject` matches the next cert’s `issuer`. To obtain the root cert, ask your Windows admin or, on the CA server, open `certlm.msc` → *Trusted Root Certification Authorities* → *Certificates*, then export the relevant root as a Base-64 encoded `.cer` file.
9. *(Linux)* Set file permissions:
```shell
chmod 644 /etc/xlwings-lite/certs/tls.crt
chmod 600 /etc/xlwings-lite/certs/tls.key
```
10. *(Linux)* Restart xlwings Lite:
```shell
xlwings-lite restart
```
11. Open `https://your-vm.example.com` in a browser. The address bar should show the padlock with no warning.
Once it works, keep the `tls.csr` and config files around for the next renewal. When the cert expires (typically 1–2 years), you’ll regenerate the CSR and request a new leaf certificate. `tls.key` can be reused indefinitely or rotated.
### TLS certificates via Let’s Encrypt (certbot)
If your VM’s hostname is a subdomain of a public domain you control (e.g. `your-vm.example.com`), you can get a free, auto-renewing certificate from Let’s Encrypt—even when the VM itself is not reachable from the public internet. Use the [DNS-01 challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge), which proves domain ownership by creating a TXT record at `_acme-challenge.your-vm.example.com` instead of requiring inbound HTTP access.
With a DNS provider plugin (e.g. `certbot-dns-cloudflare`), renewals can be fully automated:
```bash
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
-d your-vm.example.com
```
The certificate and key will be placed under `/etc/letsencrypt/live/your-vm.example.com/` as `fullchain.pem` and `privkey.pem`. Since xlwings Lite reads the cert from `/etc/xlwings-lite/certs/`, add a deploy hook that copies the renewed files into place and restarts the service:
```shell
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/xlwings-lite.sh > /dev/null <<'EOF'
#!/bin/bash
set -e
cp /etc/letsencrypt/live/your-vm.example.com/fullchain.pem /etc/xlwings-lite/certs/tls.crt
cp /etc/letsencrypt/live/your-vm.example.com/privkey.pem /etc/xlwings-lite/certs/tls.key
chmod 644 /etc/xlwings-lite/certs/tls.crt
chmod 600 /etc/xlwings-lite/certs/tls.key
xlwings-lite restart
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/xlwings-lite.sh
```
certbot’s systemd timer will then handle renewals automatically and run the hook each time the certificate is renewed.
## Troubleshooting
- **`cannot talk to the docker daemon`** — Start Docker: `sudo systemctl start docker`.
# migrating-to-self-hosted.md
# Migrating to self-hosted
If you are switching from the cloud add-in **xlwings Lite** to the self-hosted add-in **xlwings Lite (SH)**, you need to migrate existing workbooks.
After the migration, the workbooks will open with **xlwings Lite (SH)** and you can uninstall **xlwings Lite** via the add-in store or via the Microsoft 365 admin center, depending on how you installed it.
#### NOTE
You don’t need **xlwings Lite** for the migration, so you can uninstall it even before the migration.
## Convert workbooks
1. Make a backup of all the workbooks you want to migrate.
#### NOTE
If a workbook contains code from **both** add-ins (because you previously edited it with each one), only the **xlwings Lite** code is kept after migration. Code stored under **xlwings Lite (SH)** in that workbook is discarded.
2. Open a new, empty workbook, then open **xlwings Lite (SH)**. Make sure you don’t have any of the workbooks open that you want to migrate.
3. Go to **≡ > Files** and select the **Import/Export** tab. Drag and drop one or more Excel files (or folders with Excel files) onto the drop zone.

4. Click on **≡ > Convert from cloud add-in**.

5. Confirm the prompt in the Output pane. Any errors during conversion will be reported there.

6. Once done, go back to **≡ > Files > Import/Export** tab and download the now converted files (`data.zip`) by clicking on **Download All**.

7. Unzip `data.zip` (right-click > **Extract all…** > **Extract**) and replace your local workbooks with the version from the unzipped `data` folder.
## Protected View on Windows
Since xlwings Lite runs in a browser, the converted files are tagged as downloaded from the internet. On Windows, Excel will open in Protected View, requiring you to click on **Enable Editing**:

If you don’t want to do this manually for each file, run the following command in PowerShell:
```powershell
Get-ChildItem -Path "$env:USERPROFILE\Downloads\data" -Recurse -File | Unblock-File
```
Adjust the path if you saved the converted files somewhere other than `Downloads\data`.
# notebooks.md
# Notebooks
xlwings notebooks work similar to a Jupyter notebook: you split your code into cells and run them individually, in groups, or all at once, keeping the results and variables around between runs. It’s the ideal place to explore data interactively or prototype code before moving it into a [custom function](custom-functions.md) or [script](custom-scripts.md).
Unlike Jupyter notebooks, xlwings notebooks are regular Python files with the `.nb.py` extension, and code cells are defined using a `# %%` comment. The same comments are used by the [Spyder IDE](https://docs.spyder-ide.org/current/panes/editor.html#code-cells) or the [Python Interactive window](https://code.visualstudio.com/docs/python/jupyter-support-py) in VS Code.
#### NOTE
Please set xlwings to `0.36.10` or later in `requirements.txt` to work with notebooks.
## Cells
Cells are separated by a comment that starts with `# %%`. Anything before the first `# %%` is treated as the first cell. You can optionally add a title, which then shows up in the output pane.

The cell state is shown to the left of the line numbers:
∗
The cell is currently running.
✓
The cell ran successfully.
✕
The cell raised an error.
Unlike Jupyter notebooks, the numbers (`[1]`, `[2]`, etc.) indicate each cell’s current position and don’t increase with every run. This allows you to easily connect the output from the Output pane with the respective cell.
Like Jupyter, the notebook automatically displays the value of the last expression in a cell. In the screenshot above, a cell whose last line is just `df` shows the DataFrame in the output pane—you don’t need to call `print()`.
All cells share one Python namespace, so variables, imports, and functions defined in one cell are available in other cells, just as they are in Jupyter notebooks.
## Markdown cells
Besides code cells, you can add text cells for headings and explanations by tagging a cell with `[markdown]`. The body is written as regular `#` comments, which keeps the notebook a valid Python file:
```python
# %% [markdown]
# # Sales Analysis
#
# Loads the **Q3** figures and writes them to a new sheet.
```
Running a markdown cell renders it into the output pane. You can run it like any other cell, e.g., via `Shift + Enter` or the `▶ Run Cell` cell action.
Like with code cells, you can also add a title after the tag:
```python
# %% [markdown] Introduction
# Some text.
```
Markdown cells can display images from HTTPS URLs or [data URIs](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data). See [Internet access](security.md#internet-access) for privacy considerations.
## View in Excel
After running a cell, you can right-click on a variable name and select **View in Excel (new sheet)**. This writes the variable’s data to a new sheet named after the variable, e.g. `df`. Viewing the same variable again adds another sheet with a counter (`df2`, `df3`, etc.). If the variable is a pandas or Polars DataFrame, it will be formatted as an Excel table:

## Running cells
You can run cells via the run button, cell actions, or keyboard shortcuts:
### Run button
The dropdown lets you choose what the button does:
- **Run Cell**: run the active cell.
- **Run Cell & Advance**: run the active cell, then move the cursor to the next cell.
- **Reset & Run All**: reset the notebook session and run every cell from the top.
There’s also a **Replace output** toggle that controls whether running a cell replaces the previous output (default) or appends the new output below it. **Reset & Run All** always clears the previous output because it resets the notebook session.
### Cell actions
Each cell shows a set of clickable links above it:
- **▶ Run Cell**: run just this cell.
- **▲ Run Above**: run every cell above this one (not including this cell).
- **▼ Run Cell And Below**: run this cell and every cell below it.
- **+ Insert Cell**: add a new empty `# %%` cell right after this one.
- **- Delete Cell**: remove this cell.
### Keyboard shortcuts
The following keyboard shortcuts are available:
- **Shift + Enter**: run the current cell and advance, i.e., move the cursor to the next cell. If you run the last cell, a new empty cell is added at the end and the cursor moves into it (just like Jupyter).
- **Ctrl + Enter** (Windows) / **Cmd + Enter** (macOS): run the current cell without advancing.
- **F5**: run the action currently selected in the Run button.
## Plots
Notebooks support the following plotting libraries:
- [Matplotlib](https://matplotlib.org/)
- [Seaborn](https://seaborn.pydata.org/)
- [Plotly](https://plotly.com/python/)
- [Bokeh](https://bokeh.org/)
- [Altair](https://altair-viz.github.io/)
- [Plotnine](https://plotnine.org/)
- [xy](https://github.com/reflex-dev/xy)
Here are a few examples for each library:
### Matplotlib
```python
import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4, 5])
```
or, using the object-oriented interface:
```python
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot([1, 2, 3, 4, 5])
```
### Seaborn
Both the figure-level and axes-level functions work:
```python
import seaborn as sns
tips = sns.load_dataset("tips")
sns.relplot(data=tips, x="total_bill", y="tip", hue="time")
```
or, using the objects interface:
```python
import seaborn as sns
import seaborn.objects as so
tips = sns.load_dataset("tips")
so.Plot(tips, x="total_bill", y="tip").add(so.Dot())
```
### Plotly
```python
import plotly.express as px
df = px.data.iris()
px.scatter(df, x="sepal_width", y="sepal_length", color="species")
```
or
```python
import plotly.express as px
df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species")
fig.show()
```
### Bokeh
```python
from bokeh.plotting import figure
p = figure(title="Simple line example", x_axis_label="x", y_axis_label="y")
p.line([1, 2, 3, 4, 5], [6, 7, 2, 4, 5], legend_label="Temp.", line_width=2)
p
```
or
```python
from bokeh.plotting import figure, show
p = figure(title="Simple line example", x_axis_label="x", y_axis_label="y")
p.line([1, 2, 3, 4, 5], [6, 7, 2, 4, 5], legend_label="Temp.", line_width=2)
show(p)
```
### Altair
```python
import altair as alt
from altair.datasets import data
cars = data.cars()
alt.Chart(cars).mark_point().encode(
x="Horsepower",
y="Miles_per_Gallon",
color="Origin",
).interactive()
```
### Plotnine
```python
from plotnine import aes, geom_point, ggplot
from plotnine.data import mtcars
ggplot(mtcars, aes("wt", "mpg", color="factor(gear)")) + geom_point()
```
### xy
```python
import xy
xy.line_chart(xy.line([1, 2, 3, 4, 5], [120, 180, 165, 240, 310]))
```
#### NOTE
Requires Pyodide `314.0.0` or later.
## Top-level async/await support
Notebooks allow you to use `await` directly at the top level of a cell; you don’t need to wrap your code in an `async` function as required by regular Python files (e.g., `main.py`):
```python
import httpx # add httpx to requirements.txt
async with httpx.AsyncClient() as client:
response = await client.get("https://api.github.com/repos/xlwings/xlwings")
response.json()["stargazers_count"]
```
## Working with the Excel object model
In notebooks, it is recommended to work with the [async API](async-api.md):
- **Sync API**: The whole workbook is loaded the moment you call `xw.books.active`. With large worksheets, this can get slow or even run out of memory, and values can be outdated by the time you call `myrange.value`:
```python
import xlwings as xw
book = xw.books.active
data = book.sheets[0]["A1:B2"].value
```
`data` represents the state of the Excel values when you called `xw.books.active`.
- **[Async API](async-api.md) (recommended)**: Cell values are loaded on demand. This is faster for large workbooks as only the specified data is loaded. On top of that, the values always correspond to the current state in Excel:
```python
import xlwings as xw
book = await xw.books.get_active()
data = await book.sheets[0]["A1:B2"].get_value()
```
`data` represents the state of the Excel values when you called `get_value()`.
Before running a cell, the current state (except for cell values) is updated automatically. If you need to get the current state mid-cell, run:
```python
await book.load()
```
Writes to Excel are flushed automatically at the end of each cell, so you’ll see changes appear in the sheet as soon as the cell finishes running. If you need to flush data mid-cell, you can do it explicitly:
```python
await book.flush()
```
## Migrating from Python in Excel to xlwings Lite
You can migrate formulas from Microsoft’s *Python in Excel* by going to **Source files** > **Import from Python in Excel**. This scans the workbook for `=PY()` formulas, converts them into notebook cells, and writes them to a notebook called `imported.nb.py`.
## An example notebook
```python
# %% Imports
import pandas as pd
import seaborn as sns
import xlwings as xw
# %% Get the book object (Async API)
book = await xw.books.get_active()
# %% Load the penguins dataset
url = "https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv"
df = pd.read_csv(url)
df
# %% Write df to a new sheet
sheet = book.sheets.add()
sheet["A1"].value = df.dropna()
sheet.activate()
# %% Read back from Excel (Async API)
data = await sheet["A1"].options(pd.DataFrame, expand="table").get_value()
data
# %% Plot
sns.jointplot(data=data, x="flipper_length_mm", y="bill_length_mm", hue="species")
```
# personal-modules.md
# Personal Modules
Personal modules are Python files and packages shared across all your workbooks, similar to VBA’s `Personal.xlsb`. They let you define scripts and custom functions that are available in every workbook you open. Personal modules are also importable in the workbook’s `main.py`. Since the files are stored on your machine, you can track them with Git or sync them using OneDrive or a similar service.
## Setup
Create a personal folder on your local computer in the following style:
```text
personal/
accounting.py
utils.py
mypackage/
__init__.py
other.py
```
Then go to `Menu` > `Files`. Now either
- upload the folder via drag and drop under `Import/Export` or
- mount the folder as `/personal` under `Local Folders` (requires Desktop Excel on Windows)
#### NOTE
After uploading files or changing files in the mounted folder, you need to restart xlwings Lite by going to `Menu` > `Restart`.
After restarting xlwings Lite, the scripts defined in the personal modules will appear in the Run button dropdown and the custom functions will be available in your workbook.
## Projects: Workbook-specific modules
If you have modules that should only be available for specific workbooks, place them in a `projects` subdirectory:
```text
personal/
utils.py ← available to all workbooks
projects/
sales_report/ ← only for workbooks matching "sales_report"
__init__.py
charts.py
monthly_review/ ← only for workbooks matching "monthly_review"
analysis.py
```
The project directory name is fuzzy-matched against the workbook filename:
- Both names are normalized: the file extension is stripped, spaces are converted to underscores, and everything is lowercased.
- A project directory matches if its normalized name is contained in the normalized workbook name.
For example, the project directory `sales_report` matches all of these workbooks:
- `Sales_Report.xlsx`
- `2025_01_15_sales_report_v2.xlsx`
- `sales report Q1.xlsx`
- `Sales_Report_John.xlsx`
Modules placed directly in `personal` are always loaded regardless of the workbook name. Modules in `personal/projects` are loaded in addition to the global ones when the workbook name matches.
## Import order
Modules are loaded in this order:
1. Root-level personal modules (from `/data/personal` or `/personal`), alphabetically
2. Project-specific modules (from the matching `projects/` subfolder), alphabetically
3. The workbook’s `main.py`
## Limitations
- Personal modules currently don’t offer autocomplete when imported into `main.py`
- Personal modules require a restart after making changes
# plotting.md
# Plotting
xlwings Lite lets you create plots with the usual Python plotting libraries and bring them into Excel. Depending on where your code runs—a custom function, a script, or the notebook—the way you display a plot differs slightly. This page gives you an overview of all three.
At a glance:
| Where | How the plot is shown | Libraries |
|----------------------|-------------------------------------------------------------------------------------------|--------------------------------------------------|
| [Custom functions]() | Returned as an in-cell image into the calling cell. An over-the-cells image can be added. | Matplotlib\* |
| [Scripts]() | Embedded as a picture on the sheet | Matplotlib\* |
| [Notebooks]() | Rendered inline in the output pane. | Matplotlib\*, Plotly, and many others |
\* This includes all libraries that are built on top of Matplotlib, including pandas, Seaborn, Plotnine, etc.
## Custom functions
See [Plots](custom-functions.md#plots) in the custom functions tutorial.
## Scripts
In a [script](custom-scripts.md), you have access to the Excel object model, so you can embed a plot as a picture directly onto a sheet via `sheet.pictures.add()`:
```python
import matplotlib.pyplot as plt
import xlwings as xw
from xlwings import script
@script
def add_plot(book: xw.Book):
sheet = book.sheets[0]
fig, ax = plt.subplots()
ax.plot([1, 2, 3, 4, 5])
sheet.pictures.add(fig, name="MyPlot", update=True, anchor=sheet["A1"])
```
A few things to note:
- Use `name` together with `update=True` to replace an existing picture with the same name instead of adding a new one on every run.
- You can fine-tune the export with `export_options`, which is passed on to Matplotlib’s `savefig()`, e.g. `export_options={"dpi": 300}`.
## Notebooks
See [Plots](notebooks.md#plots) in the notebooks docs.
# quickstart.md
# Quick Start
xlwings Lite is an Excel add-in that you install from the Office add-in store. It’s free for personal and commercial use, with no registration or login required.
## Prerequisites
All you need is a supported version of Excel. There’s nothing else to install, as Python runs inside the add-in.
### Windows
- Microsoft 365
- Office 2021 or later
### macOS
- Microsoft 365
- Office 2021 or later
Note that the version of macOS needs to be Ventura (macOS 13) or later.
### Excel on the web
- Excel on the web is supported as long as you’re accessing it with a reasonably recent version of a modern browser.
- It even works with the [free version of Excel](https://www.microsoft.com/en-us/microsoft-365/free-office-online-for-the-web)
## Installation
1. Click on the `Add-ins` button in Excel. Usually, it is located on the `Home` tab, but if you are using an older version of Excel, it could be on the `Insert` tab.
2. Search for `xlwings`
3. Click on `Add` to install.

This will add an xlwings Lite button at the right end of the `Home` tab and open the xlwings Lite editor on the right-hand side of Excel. If the editor doesn’t show or if you closed it, you can reopen it anytime by clicking the xlwings Lite button on the `Home` tab:

After installation, `main.py` already contains sample code, so you can get started right away: type `=HELLO("World")` into a cell to call a Python custom function, or click the green **Run Hello World** button (or hit `F5`) to run a script.
## Video walkthrough
## Corporate environments
In corporate environments, your Excel add-in store may be blocked. In this case, ask your IT or Office administrator to install xlwings Lite from the Office 365 Admin Center. Alternatively, your company may prefer to self-host xlwings Lite (see [Self Hosting](self-hosting.md)).
## Uninstalling xlwings Lite
To uninstall xlwings Lite, start again by clicking on the `Add-ins` button, then click on `More Add-ins` at the bottom of the appearing menu. Under `Manage your Apps`, you’ll find xlwings Lite. Click on the three dots to the right and select `Remove`.
On older versions of Excel, you have to go to `My Add-ins` instead of `Manage your Apps`, then hover over xlwings Lite, which makes the three dots appear.
## Next steps
- [Custom Functions](custom-functions.md): Define native custom functions (user-defined functions) that you call directly from a cell.
- [Scripts](custom-scripts.md): Automate Excel by running Python scripts at the click of a button.
- [Notebooks](notebooks.md): Write and run code cell by cell, as in Jupyter notebooks.
- [Dependencies](dependencies.md): Learn how to install 3rd party libraries like pandas, Polars, or DuckDB.
# security.md
# Security
xlwings Lite runs Python code inside the browser’s sandbox, so unlike VBA macros, it has no direct access to your operating system. On top of that, code that comes from a workbook only runs after you’ve approved it. This page explains which safeguards xlwings Lite uses and what risks you’re still exposed to.
For business-critical use, [self-hosting](self-hosting.md) puts the add-in, Python runtime, and packages on infrastructure you own.
## Trust prompt
Whenever you open a workbook that contains Python code, xlwings Lite asks you whether you trust the authors of that workbook before it runs anything:

You have two options:
- **Trust and Continue**: The code is loaded and potentially runs automatically. xlwings Lite remembers your choice for this workbook, so you won’t be asked again—unless the code changes, see below.
- **Don’t Trust (View Only)**: The code is shown in a read-only editor so you can inspect it, but nothing is executed. To run it, reload the add-in and choose **Trust and Continue**. In [App Mode](app-mode.md), this option is only shown after hitting `Ctrl`+`Shift`+`M` (Windows) or `Cmd`+`Shift`+`M` (macOS).
You’ll be prompted again if:
- Someone else changes the workbook’s code
- The Office cache is cleared
- An xlwings Lite update requires it
## Local browser storage
The following data is stored unencrypted in your browser’s local storage:
- Local environment variables
- Local settings (this includes e.g., API keys for AI providers)
- Workbook trust decisions
- Wingman chat history, which can include prompts, responses, and previous or applied source code
Local storage is isolated by the browser’s origin security model, so unrelated websites and other Office add-ins cannot access this data. Environment variables and settings with workbook scope are not stored here, see [Workbook-scoped environment variables and settings]().
Files managed through **Files > Import/Export** are stored separately in the browser’s IndexedDB storage, also unencrypted. On supported versions of desktop Excel for Windows, xlwings Lite also stores handles for folders you mount through **Files > Local Folders**. The browser may require you to grant access again, but a mounted folder gives trusted workbook code read and write access to that folder. Mounts are available across workbooks, so share only the smallest folder your code needs. See [Files](files.md).
This isolation does not, however, protect against the following:
- **Workbook code**: Once trusted, code in a workbook runs under the same origin and can therefore read the data listed above, along with any folders you’ve mounted—and could send it over the network.
- **Compromised add-in code**: Malicious code introduced through Cross-Site Scripting (XSS), a compromised dependency, a compromised CDN, or a compromise of the xlwings Lite source repository or build pipeline could access browser storage. xlwings Lite reduces this risk with a restrictive Content Security Policy (CSP), [Subresource Integrity (SRI)](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) verification when loading the Pyodide runtime, and [GitHub Dependabot](https://docs.github.com/en/code-security/dependabot) alerts for known vulnerable dependencies. These measures reduce, but do not eliminate, the risk.
- **Local account access**: Anyone with access to your operating-system account or browser profile could inspect local storage.
Consider using low‑privilege, rotatable API keys and setting spending limits on LLM providers (e.g., OpenAI) to reduce potential impact.
## Workbook-scoped environment variables and settings
[Environment variables](environment-variables.md) and settings can also be given workbook scope, in which case they are stored unencrypted inside the Excel workbook itself rather than in your browser’s local storage.
Workbook-scoped values travel with the file. If you email the workbook, upload it to SharePoint, or commit it to a Git repository, everyone with access to that file also gets these values. Never store API keys, passwords, or connection strings with workbook scope in files you share.
Workbook scope is a good fit for values that aren’t secret but belong to the workbook, e.g., a base URL, a region, or a report name. For secrets, use local scope so that each user provides their own value on their own machine.
## Internet access
Python runs on the end user’s computer, so direct network requests originate from that machine and go through the same network path as other browser traffic from that computer. This means they are subject to your existing corporate network controls—HTTP proxies, TLS-inspecting gateways, firewalls, and egress filtering or allowlists all apply automatically, and requests show up in the same logs.
Markdown cells can load images from external HTTPS URLs when you run them. The request originates from your computer, so the image host can see connection metadata such as your IP address. xlwings Lite sets a no-referrer policy on these images, so the image host does not receive the add-in’s URL. Use images from [data URIs](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data) if you don’t want to contact an external host.
## Python packages
Packages installed from `requirements.txt` execute with the same capabilities as the rest of the trusted workbook code. Only install packages from sources you trust.
`requirements-pinned.txt` requires Pyodide 314.0.0+ and records package names and versions, but not wheel URLs or cryptographic hashes. Pinning makes dependency resolution more reproducible, but it does not verify the exact package artifact that was downloaded. The trust prompt covers changes to `requirements.txt` and `requirements-pinned.txt`; it cannot detect a package registry serving different bytes for an unchanged requirement.
For higher-assurance deployments, use an audited internal package registry or mirror (e.g., Azure Artifacts) and disable direct public-PyPI access (this requires [self-hosting](self-hosting.md)).
## Wingman
Wingman, the xlwings Lite AI assistant, sends your prompt, conversation context, and relevant source-code context from the editor to the AI provider and base URL you configured. The request also includes the API key for that provider. Review the provider’s data-handling and retention terms, use low-privilege and rotatable keys, and only configure custom base URLs that you trust. Wingman chat history is stored locally as described under [Local browser storage]().
## Custom functions that return images
Excel can only display an image in a cell by fetching it from a URL, so custom functions that return a Matplotlib figure need to make the rendered image reachable via a URL. xlwings Lite therefore uploads the image to the xlwings image cache, from where Excel fetches it and embeds it in the workbook; the image is deleted from the cache after it is fetched. Only the rendered PNG is uploaded automatically—not the workbook or source code.
Because of this round trip, the feature requires an internet connection during calculation. If you don’t want images leaving your machine, go to **xlwings Lite menu > Settings > Local** and turn off **Allow custom functions to return images**, see [Plots](custom-functions.md#plots).
If you [self-host](self-hosting.md), the image cache is part of your own deployment by default, so images stay on infrastructure you control and there’s nothing to enable.
## Self-hosting
Running xlwings Lite on your own infrastructure lets you serve the add-in, the Pyodide runtime, and Python packages from infrastructure you own and audit. For example, you can use an internal PyPI mirror such as Azure Artifacts to reduce supply-chain risk, and keep the image cache inside your network.
See [Overview](self-hosting.md) for the full list of benefits, the architecture, and deployment instructions.
# self-hosting.md
# Overview
This page is the starting point for deploying xlwings Lite on your own infrastructure. Self-hosting requires a Business or Enterprise plan, see [pricing](https://www.xlwings.org/pricing).
## Why self-host?
If you rely on xlwings Lite for business-critical work, self-hosting gives you control over security, availability, and compliance:
- **Security**: Your files are no longer served from public internet servers that could get hacked, and users install dependencies from an internal PyPI (*Python Package Index*) mirror (e.g., Azure Artifacts) to guard against supply-chain attacks.
- **Privacy**: Everything stays within your own network. Self-hosting even works on fully air-gapped systems with no internet access.
- **Reliability**: No reliance on third-party services that the cloud add-in uses like jsDelivr, PyPI, Cloudflare, or Microsoft Azure—an outage on any of them won’t take your users down. And if Microsoft ever blocks the add-in or suspends the developer account it’s published under, your deployment keeps running.
- **Reproducibility**: You host every file yourself, including the Python packages, so your workbooks keep working even if packages are deleted from PyPI.
- **Compliance**: Keep all data on infrastructure you can audit, helping you meet requirements like GDPR, HIPAA, or SOC 2 and your own data-governance policies.
- **Additional features**: Optionally allow users to access every web API through the baked-in CORS proxy.
- **Direct support**: Self-hosting comes with direct support.
## Architecture
xlwings Lite is an [Office.js](https://learn.microsoft.com/en-us/office/dev/add-ins/) add-in powered by [Pyodide](https://pyodide.org), a Python distribution that runs in the browser via WebAssembly (Wasm). Office.js is Microsoft’s JavaScript-based add-in technology, and because it runs in the browser, the add-in’s files must be served from a server—this is exactly what you take over when you self-host. Since Python itself runs in the user’s browser rather than on the server, that server needs only minimal resources. The following illustration helps you understand how everything fits together:
1. **Manifest (XML)**: A configuration file that defines the add-in’s button on the ribbon’s Home tab and points to the static file server—in this case, the Docker container. You install the add-in by uploading the manifest to the Microsoft 365 admin center.
2. **main.py**: All files from xlwings Lite’s code editor, including `main.py`, are stored inside the workbook itself. This makes it behave just like VBA-enabled workbooks.
3. **Browser**: Office.js add-ins run in an embedded browser—WebView2 on Windows and WKWebView on macOS. This is where Python runs via Pyodide.
4. **Caddy**: The web server that serves the add-in’s static files, including the Pyodide runtime and its packages. It also acts as an optional CORS proxy and an image cache (so custom functions can return images), and it handles access to internal PyPI registries such as Azure Artifacts.
5. **Packages Registry**: We recommend serving Python packages from an internal PyPI mirror or proxy such as Azure Artifacts.
## Hosting Solutions
xlwings Lite runs on any platform capable of running a Docker image, see [Hosting Solutions](hosting-solutions.md) for instructions on specific providers. Whichever platform you choose, see [Configuration](configuration.md) for the available settings.
# tests.md
# Testing with Pytest
xlwings Lite has a built-in [pytest](https://docs.pytest.org) runner, allowing you to write automated tests for your Python code as well as for the workbook itself.
Create one or more test files, then click the green `Run all tests` button to run them all, or pick a single test file from the dropdown to run just that file’s tests. You can also click the green play button in the gutter next to any test function to run that one test.
#### NOTE
pytest isn’t installed by default. Add `pytest` to `requirements.txt`, followed by a restart.
## Test files and functions
A *test file* is any file whose name matches pytest’s convention: `test_*.py` (e.g. `test_main.py`) or `*_test.py` (e.g. `sheet_test.py`). Create them with **New File…** in the Source files menu.
A *test function* is a function whose name starts with `test_`. Use `assert` statements to check conditions:
```python
def test_addition():
assert 1 + 1 == 2
```
Tests can also be grouped in a class, following pytest’s convention of a `Test`-prefixed name. Both sync and `async def` methods are supported, and each gets a play button in the gutter just like top-level tests:
```python
class TestMath:
def test_add(self):
assert 1 + 1 == 2
async def test_subtract(self):
assert 3 - 1 == 2
```
## Importing your own code
Tests can import functions from other modules such as `main.py`:
```python
# main.py
from xlwings import func
@func
def hello(name: str):
return f"Hello {name}!"
```
```python
# test_main.py
from main import hello
def test_hello():
assert hello("xlwings") == "Hello xlwings!"
```
## Async tests
Tests defined as `async def` are supported. However, they’re executed outside of pytest, so pytest fixtures, markers, and plugins aren’t supported. For an async class method, the class is instantiated per test, but `setup_method` and other pytest hooks don’t run.
```python
import xlwings as xw
async def test_values():
book = await xw.books.get_active() # See docs about Async API
values = await book.sheets[0]["A1:B2"].get_value()
assert values == [[1, 2], [3, 4]]
```
## Accessing the active workbook
Both the sync API (`xw.books.active`) and async API (`await xw.books.get_active()`) work in tests. The sync API fetches values for every sheet — for large workbooks, prefer `await xw.books.get_active()` from an async test, which loads on demand, see previous example and [Async API](async-api.md).
## Configuring pytest
Add a `[tool.pytest]` section to `pyproject.toml`. For example:
```toml
[tool.pytest]
addopts = "-s" # show print() output from tests
```
Restart xlwings Lite after changing `pyproject.toml`.
#### NOTE
* Async tests don’t go through pytest, so options under `[tool.pytest]` only affect sync tests.
* `[tool.pytest]` is only supported with pytest 9.0+. Pyodide 0.27.5 comes with an older version of pytest, so use `[tool.pytest.ini_options]` instead (you can change your Pyodide version under **xlwings Lite menu** > **Settings** > **Workbook**).
## Limitations
- Async pytest plugins like `pytest-asyncio` are [not supported](https://github.com/pytest-dev/pytest-asyncio/issues/1333) — xlwings Lite runs async tests outside of pytest instead.
- `conftest.py` is supported for sync tests. Its fixtures and hooks don’t apply to async tests, since those run outside of pytest.
# troubleshooting.md
# Troubleshooting
## pyodide.ffi.JsException: NetworkError: A network error occurred.
Most likely, you’re running into a CORS issue, see [Web API Requests](web-requests.md)
## Error initializing application: undefined is not an object (evaluating ‘editorInstance.setScrollPosition’)
Your browser engine (“WebView”), which powers modern Excel add-ins like xlwings Lite, is too old.
- For the best experience, use a perpetual version of Office no older than Office 2021 or an up-to-date version of Microsoft 365.
- On macOS, Ventura is the oldest version that is supported as the OS alone determines the version of WKWebView.
- On Windows, Office 2019 is supported under [certain conditions](https://learn.microsoft.com/en-us/office/dev/add-ins/concepts/browsers-used-by-office-web-add-ins).
In any case, you can always use xlwings Lite by going to [https://office.com](https://office.com) with a reasonably modern browser (this also works with the free online version of Excel).
## Unexpected error: SSL is not supported
With Pyodide 0.27.5 only: Add `ssl` explicitly to `requirements.txt`.
## The cell shows `#NAME?` when using custom functions
Clear your Office cache, see [Clear your Office cache]()
## Clear your Office cache
### Windows
1. Go to `File` > `Options` > `Trust Center` > `Trust Center Settings` > `Trusted Add-in Catalogs`
2. Activate the checkbox `Next time Office starts, clear all previously-started web add-ins cache.`, then click `OK`.
3. Restart Excel
Alternatively:
1. Close Excel
2. Delete the following folder:
```text
%LOCALAPPDATA%\Microsoft\Office\16.0\Wef
```
### macOS
- Close Excel via `Cmd+Q`
- Run the following commands in a Terminal:
```text
rm -rf ~/Library/Containers/com.microsoft.Excel/Data/Library/Caches/
rm -rf ~/Library/Containers/com.microsoft.Excel/Data/Library/Application
rm -rf ~/Library/Containers/com.microsoft.Office365ServiceV2/Data/Caches/com.microsoft.Office365ServiceV2/
rm -rf ~/Library/Containers/com.microsoft.Office365ServiceV2/Data/Library/Caches/com.microsoft.Office365ServiceV2/
rm -rf ~/Library/Containers/com.microsoft.Excel/Data/Library/Application\ Support/Microsoft/Office/16.0/Wef/
```
See also: [https://learn.microsoft.com/en-us/office/dev/add-ins/testing/clear-cache](https://learn.microsoft.com/en-us/office/dev/add-ins/testing/clear-cache)
# web-requests.md
# Web API Requests
xlwings Lite runs Python in the browser, which means that any API requests you make are subject to the same security constraints that apply to browser-based applications.
For security reasons, browsers only allow you to make requests to the same origin, i.e., domain, where your application is hosted, unless the server explicitly allows it by setting CORS (Cross-Origin Resource Sharing) headers in its response (like `Access-Control-Allow-Origin`).
Here’s what this means:
- Public APIs such as GitHub often set the CORS headers. So the following will work:
```python
import pandas as pd
df = pd.read_csv(
"https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv"
)
```
- If your API request fails with CORS errors and you control the server you’re trying to access, configure it to add the following header to responses:
```ini
Access-Control-Allow-Origin: https://addin.xlwings.org
```
If you’re self-hosting the xlwings Lite add-in, use your own domain instead (e.g., `https://xlwings-lite.mycompany.com`).
- If your API request fails with CORS errors and you don’t control the server, you can use a CORS proxy. This is a backend server that forwards your requests to the target API and adds the required CORS headers to the responses. While commercial proxy services exist, for security and data privacy reasons, it’s recommended to host your own CORS proxy rather than sending potentially sensitive data through third-party servers.
- If you use the [self-hosted version of xlwings Lite](self-hosting.md), you can use its built-in CORS proxy, see [CORS proxy](configuration.md#cors-proxy).