written by Eric J. Ma on 2025-04-08 | tags: marimo reactive notebooks uv deployment serverless data science modal
In this blog post, I share my experience with Marimo notebooks, highlighting their fully reactive nature and self-contained environments. I discuss how to run Marimo without installation using uv
, and the benefits of AI-assisted coding. I also cover exporting notebooks to Markdown and deploying them as Modal apps. While Marimo's keybindings differ from Jupyter, its reactive execution and UI builder offer unique advantages. Curious about how Marimo can transform your coding workflow?
I have been using Marimo notebooks recently, and I'm thoroughly impressed. There are many benefits to using marimo
. The biggest are fully reactive notebooks, such that if you change a cell and execute it, all cells that depend on it will automatically re-execute, and self-contained notebook environments, so you never have to create a scratch environment to run a notebook. If you haven't started test-driving Marimo to see whether it works for you, I think it's time to start experimenting!
Thanks to uv
, we can run marimo
without ever needing to explicitly install it. This is a major upgrade from installing it with pip
/conda
and having to remember where on my PATH environment variable marimo
is. No more asking the question, "Did I install it in my base conda environment? Or was it installed in another env?"
To do this, with uv
installed on your PATH
:
uvx marimo edit --sandbox /path/to/notebook.py
The notebook will get created automatically if it doesn't exist.
What I love about this is that I get to use the latest marimo
all the time. And I don't have to think about what version of marimo
I'm carrying around in my local machine. uv
is providing the equivalent of a serverless API for CLI tools.
Marimo notebooks can be self-contained, with Python dependencies fully-specified in-file with PEP723-compatible in-line script metadata. The way to ensure that this is done is by running exactly the command above, with --sandbox
being the key. Additionally, if you add packages to the self-contained notebook via UI, they automagically get added into the in-line script metadata. The resulting file looks something like this:
# /// script # requires-python = ">=3.12" # dependencies = [ # "anthropic==0.49.0", # "arviz==0.21.0", # "joypy==0.2.6", # "jupyter-core==5.7.2", # "marimo", # "matplotlib==3.10.1", # "nbformat==5.10.4", # "numba==0.61.0", # "numpy==2.1.0", # "nutpie==0.14.3", # "pandas==2.2.3", # "pymc==5.21.1", # "seaborn==0.13.2", # "tqdm==4.67.1", # ] # /// import marimo __generated_with = "0.12.2" app = marimo.App(width="medium") @app.cell def _(): import marimo as mo return (mo,) # ...the rest of the notebook below...
Also, you can specify alternate sources in-line, with this example coming from my Network Analysis Made Simple repository:
# /// script # requires-python = ">=3.13" # dependencies = [ # "ipython==9.1.0", # "marimo", # "matplotlib==3.10.1", # "nams==0.0.2", # "networkx==3.4.2", # "numpy==2.2.5", # "pyprojroot==0.3.0", # "tqdm==4.67.1", # ] # [[tool.uv.index]] # name = "ericmjl-personal-packages" # url = "https://ericmjl--pypiserver-server.modal.run/simple/" # explicit = true # [tool.uv.sources] # nams = { index = "ericmjl-personal-packages" } # ///
You can interact with your local package in editable mode with Marimo notebooks. To do so you can add the package using the packages
tab in the UI:
-e <package-name> @ .
It will be installed in editable mode, and it will be added to the in-line script metadata:
# /// script # requires-python = ">=3.12" # dependencies = [ # "<package-name>==<version-in-pyproject.toml>", # "marimo", # ... # ] # ///
At this point, for anyone experienced enough in code writing, writing code with AI assistance is pretty much table stakes. (If you are just starting out, I would still encourage you to seek out a human mentor to teach you good patterns for writing reliable code!) Marimo has support for AI-assisted coding, and though it has some rough spots, I think it's worth taking a look at.
To enable AI assistance, you'll need an API key for one of the major API providers (OpenAI, Anthropic, Google), and you can enable code completion using GitHub Copilot, Codeium, or Ollama (custom).
That said, having gotten used to Cursor's more interactive style of coding assistance, I found Marimo's implementation of AI assistance to be a tad constraining. I can't do multi-cell edits, for example, and the connection to GitHub Copilot (for inline assistance) often shows an error connecting. My workaround for now has been to write a bunch of cells in Marimo, and then switch over to Cursor to directly edit the .py
file (e.g. to condense it to be less verbose, or correct inconsistencies I might have accumulated).
uvx marimo edit --sandbox https://url.to/your/notebook.py
This is so, so, so, so powerful and convenient! Simply point marimo
to an existing URL that constitutes the notebook, and marimo
will clone it and run it within a sandboxed environment for you. There is also an option there to run it within a Dockerized container of its own too, so it'll be entirely fenced off from your system.
When I wrote my blog post on the use of the R2D2 prior and Bayesian probability of superiority calculation, I used marimo
alongside uvx
to write the post with prose alongside code. One thing I wanted to do was to export it as a Markdown file with cell execution outputs, but Marimo's markdown exports don't carry the capability to do so natively.
Pre-requisites:
nbformat
as part of its PEP723-style dependency declaration, anduv
installed on your system.Firstly, export Marimo notebook while including outputs to a Jupyter notebook:
uvx marimo export ipynb /path/to/notebook.py -o /path/to/notebook.ipynb --include-outputs --sandbox
Secondly, export the Jupyter notebook to Markdown:
uvx --with nbconvert --from jupyter-core jupyter nbconvert --to markdown /path/to/notebook.ipynb
And in this way, your notebook will be exportable to Markdown with outputs from the notebook cells. It took me a little while to figure this out, but now that I did, I'm glad I have it as an option, as I can now write entire notebooks in Marimo notebooks and have them exportable to my blog or eBooks.
As it turns out, with Modal's ability to serve up any arbitrary web server, we can deploy Marimo notebooks to Modal easily.
Given a Marimo notebook that looks like this:
# file: demo.py # /// script # requires-python = ">=3.12" # dependencies = [ # "anthropic==0.49.0", # "pandas==2.2.3", # ] # /// import marimo __generated_with = "0.12.2" app = marimo.App(width="medium", layout_file="layouts/demo.grid.json") @app.cell def _(): import pandas as pd return (pd,) @app.cell def _(pd): data = { 'Name': ['John', 'Emma', 'Michael', 'Sarah', 'David'], 'Age': [25, 30, 35, 28, 32], 'City': ['New York', 'London', 'Paris', 'Tokyo', 'Berlin'], 'Salary': [50000, 60000, 75000, 55000, 65000] } df = pd.DataFrame(data) df return data, df if __name__ == "__main__": app.run()
And a Modal deployment script that looks like this:
# file: deployment.py import modal image = ( modal.Image.debian_slim() .pip_install(["uv"]) .add_local_file("demo.py", "/app/demo.py", copy=True) .add_local_dir("layouts", "/app/layouts", copy=True) .workdir("/app") ) app = modal.App(name="marimo-app") @app.function(image=image, allow_concurrent_inputs=100) @modal.web_server(8000, startup_timeout=60) def marimo_app(): print("Hello, World!") import subprocess # Port must match the web_server port, and host must be 0.0.0.0 for this to work. cmd = "uvx marimo run demo.py --sandbox --port 8000 --host 0.0.0.0 --headless" subprocess.Popen(cmd, shell=True)
One can iterate quickly on the notebook in Marimo, and then once you're ready, check that the deployment works (for fast iteration):
modal serve deployment.py
And to do the final deployment (best done via GitHub CI/CD):
name: CI/CD on: push: branches: - main pull_request: branches: - main jobs: deploy: name: Deploy runs-on: ubuntu-latest env: MODAL_TOKEN_ID: ${{ secrets.MODAL_TOKEN_ID }} MODAL_TOKEN_SECRET: ${{ secrets.MODAL_TOKEN_SECRET }} DEPLOY_ENV: ${{ github.event_name == 'pull_request' && 'test' || 'main' }} steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Install the latest version of uv uses: astral-sh/setup-uv@v5 with: version: "latest" - name: Install Modal run: | uv tool install modal - name: Deploy job run: | echo "Deploying with $DEPLOY_ENV environment" modal deploy deployment.py -e $DEPLOY_ENV
This gives you an outlet to deploy Marimo apps in a completely serverless fashion. Super good for runtime costs!
But Modal apps can run in WASM already, though, so why bother? It turns out not every Python package is built with WASM as a target or has issues running in WASM (PyMC is one of the latter), so this gives us an alternative way of sharing notebooks with others.
Here are some things that I know I had to get used to with Marimo:
Esc+a
or Esc+b
to quickly add a new cell above or below my current cell. Cmd+Enter
is what's used, not Ctrl+Enter
. Having default keybindings that were different was a weird design choice, and I would love to see keybinding "profiles" enabled on Marimo that let me quickly switch them out to be Jupyter-compatible. Specifically, the ones I really want to have are:Esc+a
/Esc+b
to quickly switch from cell editing mode to cell selection mode, and then to add above/below current cell,Enter
to go from cell selection mode to cell editing mode,Ctrl+Enter
to execute a cell,Cmd+I
or Cmd+K
to reveal the prompt for cell generation mode with AI,@article{
ericmjl-2025-wow-marimo,
author = {Eric J. Ma},
title = {Wow, Marimo!},
year = {2025},
month = {04},
day = {08},
howpublished = {\url{https://ericmjl.github.io}},
journal = {Eric J. Ma's Blog},
url = {https://ericmjl.github.io/blog/2025/4/8/wow-marimo},
}
I send out a newsletter with tips and tools for data scientists. Come check it out at Substack.
If you would like to sponsor the coffee that goes into making my posts, please consider GitHub Sponsors!
Finally, I do free 30-minute GenAI strategy calls for teams that are looking to leverage GenAI for maximum impact. Consider booking a call on Calendly if you're interested!