Marimo 采用反应式执行模型 :单元格间通过变量引用构成 DAG,修改上游会自动重算下游,彻底消除隐藏状态。
Notebook 存储为纯 Python 文件 (.py),支持 python notebook.py 直接运行为脚本,Git diff 干净可读。
内置 SQL 单元格 ,默认后端为 DuckDB,也可连接 PostgreSQL / MySQL / SQLite,查询结果自动返回 Polars 或 pandas DataFrame。
提供 mo.ui.slider、mo.ui.dropdown 等声明式 UI 元素 ,无需 callback 即可与 Python 变量双向绑定。
可通过 marimo run 部署为 Web 应用,或通过 marimo export html-wasm 编译为在浏览器中运行的 WebAssembly 应用。
提供 marimo convert your_notebook.ipynb 命令,可将现有 Jupyter Notebook 一键迁移。
本文目录
什么是 Marimo:反应式 Notebook 的定义
反应式执行如何工作:从 AST 到 DAG
Marimo 与 Jupyter 有什么区别
安装 Marimo 与第一个 Notebook
交互式 UI 元素:无 callback 的响应式界面
内置 SQL 单元格与 DuckDB 集成
如何将 Jupyter Notebook 转换为 Marimo
部署为 Web 应用与 WebAssembly
与 Claude Code、Copilot 等 AI 助手协作
生产环境最佳实践
什么是 Marimo:反应式 Notebook 的定义
Marimo(发音 /məˈriːmoʊ/)是由 Akshay Agrawal 等人在 2023 年发起的开源项目,其目标是重新定义 Python Notebook 的编程模型。传统的 Jupyter Notebook 采用"命令式、顺序执行"模型:单元格的执行顺序由用户点击"运行"按钮的顺序决定,运行时状态存储在全局命名空间中,且不与代码文本同步。这种设计导致了两个学术界反复讨论的问题:隐藏状态(hidden state) 与不可复现性(irreproducibility) 。
Pimentel 等人在 2019 年的 MSR 会议论文《A Large-Scale Study About Quality and Reproducibility of Jupyter Notebooks》中分析了 GitHub 上 130 多万个 Jupyter Notebook,发现只有约 24% 能顺利执行,仅约 4% 能完整复现原始输出 。这个数据长期以来被视为 Jupyter 生态的一根刺,而 Marimo 正是从模型层面尝试拔掉这根刺。
Marimo 的核心承诺是:Notebook 的可见代码就是它的全部状态 。所有单元格构成一个由变量引用推导出的有向无环图(DAG) ,执行顺序由这个图确定,而不是由用户的点击顺序决定。这一设计借鉴了 Excel 电子表格,以及 Observable、Pluto.jl(Julia)等反应式笔记本的成功经验,并把它们带入 Python 生态。
反应式执行如何工作:从 AST 到 DAG
要理解 Marimo,得先理解它的执行引擎。当你保存一个 Notebook 时,Marimo 会对每个单元格做以下事情:
使用 Python 内置的 ast 模块解析源码,得到抽象语法树。
遍历 AST,收集定义 (Store 节点,即赋值目标)与引用 (Load 节点,即读取的全局符号)。
用这些"定义 → 引用"关系构建 DAG。若单元格 A 定义了变量 df,单元格 B 读取 df,则边 A → B 存在。
当 A 被重新执行,B 会被自动调度重跑;如果配置为惰性(lazy)模式,则 B 被标记为"过时(stale)",等待手动触发。
这个模型带来一个必然约束:同一变量名只能在一个单元格中被定义 。若两个单元格都写 x = 1,Marimo 会报硬性错误 而不是让它们竞争。老实说,这一点常让 Jupyter 用户初次上手时不太适应,但从可维护性角度看,它把"顺序敏感的 bug"转化为编辑时立即可见的错误,也算符合 Python 之禅第 3 条 "Explicit is better than implicit"。
# 单元格 1
import polars as pl
df = pl.read_csv("sales.csv")
# 单元格 2,修改这个单元格会自动触发单元格 3 重跑
threshold = 1000
# 单元格 3,依赖 df 和 threshold
big_orders = df.filter(pl.col("amount") > threshold)
big_orders.head()
注意: Marimo 的 DAG 分析在模块级作用域 进行;函数内部的局部变量不会被追踪。这意味着你可以在函数体内自由使用同名局部变量,不会与其他单元格冲突。
Marimo 与 Jupyter 有什么区别
作为一名长期依赖 Jupyter 教学统计与机器学习的研究者,我在 2024 年底开始把日常的实验笔记逐步迁移到 Marimo。下面这张对比表汇总了两者在 2026 年最主要的差异,之后我会展开谈几个最容易被忽视的维度。
维度 Jupyter Notebook Marimo
执行模型 命令式、顺序敏感 反应式 DAG,自动重算下游
文件格式 JSON(.ipynb) 纯 Python(.py)
Git diff 包含 base64 输出、执行计数,难以审查 只包含代码,diff 清晰
作为脚本运行 需 jupyter nbconvert --to script 转换 python notebook.py 直接执行
UI 组件 依赖 ipywidgets,需手写 callback 内置 mo.ui.*,声明式绑定
SQL 支持 需 %%sql magic 或第三方扩展 原生 SQL 单元格,DuckDB 默认后端
部署为 Web 应用 Voilà、Panel 等外部方案 marimo run 单命令部署
浏览器内运行 JupyterLite(Pyodide) WebAssembly 一键导出
包管理 无内建 支持 uv、PEP 723 内联元数据
如果你想在 Streamlit、Dash 之间做进一步选择,可以先读我们此前发布的 Streamlit 完全指南:Python 构建数据科学 Web 应用 。Marimo 与 Streamlit 的定位其实有意思地重叠:前者从"笔记本升级为应用",后者从"应用退化到脚本",最终在反应式 UI + 纯 Python 文件 这一点上合流。
隐藏状态问题的严格定义
"隐藏状态"(hidden state)在文献中通常指:Notebook 的运行时命名空间与其源码文本不一致的现象。典型场景是:你在单元格 5 中定义 threshold = 1000,运行后又把该行删掉,但 threshold 仍在内核内存中;下游任何引用它的代码"看起来能跑",实则依赖一个已不存在的定义。任何后续拿到这份 .ipynb 的同事都无法复现结果,这就是 96% 的复现失败率的根源。
Marimo 在删除单元格时会自动从命名空间中清除其定义的变量 ,从机制上杜绝这种漂移。如果你从事需要通过审计或同行评议的分析工作(例如临床试验、金融回测、监管报告),仅这一条特性就足以说服你迁移。
安装 Marimo 与第一个 Notebook
Marimo 支持 Python 3.9 及以上版本,跨平台兼容 macOS、Linux、Windows。推荐使用 uv 或 pipx 安装到隔离环境,避免污染全局解释器:
# 使用 uv(速度最快,推荐)
uv tool install marimo
# 或使用 pipx
pipx install marimo
# 或经典 pip
pip install marimo
# 验证安装
marimo --version
创建并打开新 Notebook:
# 打开自带的交互式教程
marimo tutorial intro
# 新建一个空 Notebook
marimo edit my_analysis.py
# 以只读"应用模式"运行,隐藏源码
marimo run my_analysis.py
# 在无沙盒的 venv 中运行
marimo edit --sandbox my_analysis.py
浏览器会自动打开 http://127.0.0.1:2718。请注意端口号 2718 ,这是自然常数 e 的前四位,是社区在幽默方面的小彩蛋,也间接暗示了它的目标受众:科学计算与数据分析用户。
Marimo 单元格的解剖结构
保存后打开生成的 my_analysis.py,你会看到它是一个完全合法的 Python 模块:
import marimo
__generated_with = "0.16.0"
app = marimo.App(width="medium")
@app.cell
def _():
import polars as pl
return pl,
@app.cell
def _(pl):
df = pl.read_csv("sales.csv")
df
return df,
if __name__ == "__main__":
app.run()
每个单元格被编译为一个函数,函数参数即为该单元格的"输入变量"(其他单元格的定义),返回值即为"输出变量"。这是 Marimo 能被 Python 解释器直接运行的关键:它并不发明新的文件格式,而是把反应式语义映射到函数依赖上。
交互式 UI 元素:无 callback 的响应式界面
在传统 Jupyter 中,交互式滑块需要写 ipywidgets.interact 或注册 observe callback,代码分散且不易维护。Marimo 提供了 mo.ui 命名空间,将 UI 元素抽象为"值 + 视图"的双向绑定对象。下面示例让读者拖动滑块即可动态调整正态分布的方差并重绘直方图:
import marimo as mo
import numpy as np
import matplotlib.pyplot as plt
# 单元格 A:定义 UI
sigma = mo.ui.slider(start=0.1, stop=3.0, step=0.1, value=1.0, label="σ (标准差)")
n = mo.ui.number(value=1000, start=100, stop=10000, step=100, label="样本量 n")
mo.hstack([sigma, n])
# 单元格 B:依赖 sigma 和 n
rng = np.random.default_rng(seed=42)
samples = rng.normal(loc=0.0, scale=sigma.value, size=n.value)
fig, ax = plt.subplots(figsize=(6, 3.5))
ax.hist(samples, bins=40, alpha=0.75, edgecolor="white")
ax.set_title(f"N(0, {sigma.value:.2f}²) 样本 n={n.value}")
ax.set_xlabel("x"); ax.set_ylabel("频数")
fig
只要你拖动滑块,单元格 B 就会自动重算并更新图像,不需要 @interact,也不需要 callback。这背后仍然是那张 DAG:sigma.value 与 n.value 的变化被视为对上游变量的写入,触发下游重算。
提示: 如果你的可视化涉及大数据集重绘代价高昂,请把绘图代码与 UI 拆到不同单元格,并给昂贵计算加上 @mo.cache 装饰器(Marimo 0.10+ 提供),只在依赖真正改变时才重算。
常用 UI 组件覆盖了数据科学工作流的绝大多数交互需求:slider、range_slider、number、text、text_area、dropdown、multiselect、date、file、data_explorer、plotly、altair_chart 等。你也可以通过 mo.ui.form 将多个元素组合成一次性提交的表单,避免中间态触发昂贵计算(这一点在训练重型模型时特别有用)。
内置 SQL 单元格与 DuckDB 集成
Marimo 是首个把 SQL 提升为一等公民 的 Python Notebook:SQL 单元格并非通过魔法命令(%%sql)实现,而是编译进底层 .py 文件的合法 Python 表达式。默认后端是 DuckDB。如果你还没深入了解 DuckDB,可以先读我们的 DuckDB 完全实战指南 。
在 Marimo 编辑器里点击 "+" → "SQL cell" 即可创建 SQL 单元格。写入以下查询:
-- Marimo SQL 单元格:查询 Polars DataFrame
SELECT
region,
COUNT(*) AS n_orders,
SUM(amount) AS total,
AVG(amount) AS avg_amount
FROM df -- df 是上游 Python 单元格定义的 Polars DataFrame
WHERE amount > {{ threshold.value }} -- 引用 UI slider
GROUP BY region
ORDER BY total DESC;
这里有两个关键点:
零拷贝互操作 :Marimo 通过 DuckDB 的 replacement_scan 机制识别 Polars / pandas / PyArrow DataFrame,无需显式注册即可直接在 SQL 中引用变量名。
参数注入 :使用 {{ python_expr }} 语法把 Python 值注入 SQL。这是安全的:Marimo 会把值作为 DuckDB 的 prepared statement 参数传入,不会拼接字符串,因此不存在 SQL 注入风险(这与手写 f-string 拼接 SQL 有本质区别)。
结果自动返回为 Polars DataFrame,可以直接被下游 Python 单元格消费。你也可以在连接管理器 中配置 PostgreSQL、MySQL、SQLite、MotherDuck 等远程后端,一行 ATTACH 即可切换数据源。对于我这样的统计工作流,SQL + Polars 的组合意味着可以在同一 Notebook 内完成:DuckDB 做重型聚合、Polars 做 Polars 表达式建模 、matplotlib / altair 做可视化,全程不出 Notebook。
如何将 Jupyter Notebook 转换为 Marimo
Marimo 提供一键迁移命令:
# 转换单个文件
marimo convert your_notebook.ipynb -o your_notebook.py
# 批量转换目录下所有 .ipynb
find . -name "*.ipynb" -exec marimo convert {} \;
转换器会做以下工作:
解析 .ipynb JSON,提取每个代码单元格。
为每个单元格生成 @app.cell 函数。
Markdown 单元格转换为 mo.md(r"...") 调用。
输出(图像、表格)不迁移,它们会在首次运行时重新生成。
警告: 如果原 Notebook 存在同名变量在多个单元格中被赋值 的情况(Jupyter 允许,Marimo 禁止),转换后编辑器会显示红色下划线错误。此时需重命名或用函数封装局部作用域。我上次迁移一份四年前的旧笔记时,一次性收到 17 个这类错误。与其抱怨,不如把它当作一次代码审计的机会。
常见的迁移障碍及解决方案:
循环导入 / 全局状态修改 :把状态修改封装到函数或类中,只在单元格中调用;避免直接在模块级 mutation。
依赖执行顺序的教学示例 :Marimo 提供惰性模式(Runtime Config → Auto instantiate: off),可保留手动触发的语义用于课堂演示。
使用 IPython magics (如 %matplotlib inline):这些通常不必要,Marimo 默认会内联显示 matplotlib 图形。
部署为 Web 应用与 WebAssembly
Marimo 的部署路径有三种,各自适合不同场景。
1. 服务端部署(marimo run)
# 在 8080 端口以只读应用模式启动
marimo run my_dashboard.py --port 8080 --host 0.0.0.0
# Docker 化部署
cat > Dockerfile <<'EOF'
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt marimo
COPY my_dashboard.py .
EXPOSE 8080
CMD ["marimo", "run", "my_dashboard.py", "--port", "8080", "--host", "0.0.0.0"]
EOF
此模式下 Marimo 启动一个 Starlette 服务器,用户看到的是完整交互界面但源码隐藏。适合内部数据看板、需要长期存活的会话应用,或者需要 GPU 后端的机器学习演示。
2. WebAssembly 静态导出(无服务器)
# 编译为纯静态 HTML + WASM
marimo export html-wasm my_dashboard.py -o dist/
# 结果可直接部署到 GitHub Pages、Cloudflare Pages、S3 等
python -m http.server 8000 --directory dist
WASM 模式借助 Pyodide 让 Python 直接在浏览器沙盒中运行,无需任何服务器。适合公开分享的教学 Demo、Blog 内嵌交互组件、离线可用的分析工具。限制是不能使用需 C 扩展但未被 Pyodide 打包的库;主流数据科学栈(NumPy、pandas、Polars、scikit-learn、matplotlib)都已支持。
3. 作为 Python 脚本执行
# 无需 Marimo 运行时,直接跑 DAG
python my_analysis.py
# 结合 uv 与 PEP 723 内联元数据,一行运行含依赖
uv run --with polars,duckdb my_analysis.py
这一模式让 Notebook 天然融入 CI/CD:夜间批处理、pytest 断言、Airflow / Prefect 任务节点都可以直接调用 .py 文件,把 Notebook 从"可视化玩具"变为"生产可执行单元"。
与 Claude Code、Copilot 等 AI 助手协作
Marimo 从 0.9 版本起把 AI 集成作为一级特性。因为 Notebook 是纯 Python 文件,任何理解 Python 的 AI 都能读写它,无需专门的 JSON 解析器。在实际研究工作中,我最常用的三种协作方式如下:
编辑器内置 AI 面板 :在编辑器右侧唤起,可选 OpenAI、Anthropic、本地 Ollama 等后端。它能看到当前所有变量的类型与形状(ndim、dtype、shape),因此生成的代码比在传统 IDE 中"盲写"更贴合数据。
marimo pair 结伴模式 :与外部 CLI agent(如 Claude Code、Codex)结伴:agent 编辑 .py 文件,Marimo 编辑器实时热重载。适合让 agent 完成大段重构,你在浏览器里立即看到反应式重算的结果。
molab 云端分享 :类 Colab 的托管环境,可一键把本地 Notebook 上传为可分享链接,对开源用户免费开放。
对于希望把 Notebook 交给 LLM 修改的团队,Marimo 的纯文本格式还带来一个副产物:Git 冲突可控。Jupyter 的 JSON 格式在多人协作中几乎无法 diff,而 Marimo 的 .py 与常规代码文件一样处理。
生产环境最佳实践
过去一年半我在两个项目中把 Marimo 从实验推向生产(一个是每周一次的 A/B 报告自动化,一个是内部模型监控面板)。以下是踩坑总结:
把重型计算与 UI 拆分到不同单元格 。Marimo 会重算所有下游,如果模型训练与滑块在同一格,每次拖动都会重训。用 @mo.cache,或把训练结果存磁盘。
PEP 723 内联依赖 。在文件顶部添加 # /// script ... # /// 元数据块,用 uv run 启动时会自动创建隔离环境,团队成员不用担心依赖漂移。
用 pytest 断言 Notebook 的输出 。因为 .py 可被导入,你可以在测试中调用 from my_notebook import app、执行 app._run() 并断言关键中间变量的取值。
版本控制不要提交 __marimo__/ 缓存目录 。加入 .gitignore。
教学场景考虑关闭自动实例化 (--sandbox 或 config),避免学生一打开 Notebook 就触发耗时训练。我在给研究生上课时踩过这个坑:一整间教室的笔记本电脑同时拉起一个 3 GB 的模型加载,你懂的。
更多细节可参考 Marimo 官方文档 与 GitHub 仓库 。后者的 Discussions 区是官方推荐的求助场所,核心团队响应通常在 24 小时内。
常见问题解答
Marimo 是免费开源的吗?
是的。Marimo 采用 Apache 2.0 许可证在 GitHub 上开源,个人和商业使用均免费,无功能阉割版本。托管的云端服务 molab 目前对开源用户免费开放。
Marimo 支持在 VS Code 或 PyCharm 中使用吗?
支持。官方提供 VS Code 扩展与 PyCharm 插件,可以在 IDE 内直接编辑 .py Notebook 并预览反应式输出。你也可以任意用普通文本编辑器修改 .py,因为它就是合法 Python 文件。
Marimo 能与 Pandas 和 Polars 同时使用吗?
可以。Marimo 对 DataFrame 库无偏见,pandas、Polars、PyArrow、DuckDB 结果都能在内置数据查看器中原生渲染。SQL 单元格默认把结果返回为 Polars DataFrame,但可在配置中改为 pandas。
为什么 Marimo 不允许两个单元格定义同名变量?
因为 Marimo 通过变量引用推导执行 DAG,若同一变量有两个来源,图就不再是单一 DAG,反应式语义无法保证一致性。这个"限制"实际上把 Jupyter 中最常见的 bug 类别(覆盖式赋值 + 执行顺序错乱)从运行时错误提前到编辑时错误。
Marimo 与 Streamlit、Dash 是竞争关系吗?
存在部分重叠但定位不同。Streamlit / Dash 是"应用优先"框架,代码结构围绕页面构建;Marimo 是"Notebook 优先",先做探索,再原地部署为应用。如果你从数据探索起步,Marimo 让你少切一次工具;如果只做产品化仪表板,Streamlit 更成熟。
Marimo 支持 R 或其他语言吗?
目前 Marimo 仅支持 Python 与 SQL 单元格。R 用户可以在 Python 单元格内通过 rpy2 桥接,但没有原生的 R 单元格。官方路线图未公开 R 支持计划。