Marimo: Notebook Python yang Reaktif dan Reproducible
Marimo adalah notebook Python yang menyimpan isinya sebagai berkas .py biasa dan menjalankan sel secara reaktif, mirip cara spreadsheet menghitung ulang rumus yang saling bergantung. Marimo dirancang untuk mengatasi masalah yang sering muncul pada alur kerja notebook tradisional, terutama hidden state dan eksekusi yang tidak berurutan. Tutorial ini menjelaskan apa itu Marimo, mengapa modelnya berbeda dari Jupyter, dan cara membangun notebook eksplorasi data interaktif yang juga bisa dijalankan sebagai skrip maupun sebagai aplikasi web.
Mengapa Perlu Notebook Lain?
Notebook populer untuk eksplorasi, pengajaran, dan pelaporan. Namun notebook juga terkenal dengan satu kelas bug tertentu yang berasal dari cara kerja model notebook klasik. Sebelum melihat fitur Marimo, ada baiknya kita namai dulu masalah yang ingin diselesaikannya.
Hidden State
Pada sesi Jupyter pada umumnya, kernel menyimpan setiap variabel yang pernah Anda definisikan, bahkan setelah sel yang membuatnya dihapus. Sebuah notebook bisa tampak berfungsi hanya karena sel yang kini sudah dihapus pernah dijalankan. Buka kembali nanti, jalankan dari atas ke bawah, dan notebook itu gagal. Kode yang terlihat tidak lagi cocok dengan keadaan runtime.
Eksekusi Tidak Berurutan
Jupyter mengizinkan Anda menjalankan sel dalam urutan apa pun. Penghitung In [n] mencatat urutan yang kebetulan Anda pakai, bukan urutan yang reproducible. Dua orang yang menjalankan notebook yang sama bisa memperoleh hasil berbeda tergantung sel mana yang mereka jalankan dan kapan.
Diff dan Version Control yang Buruk
Berkas .ipynb adalah JSON yang menyatukan kode sumber, penghitung eksekusi, dan output ter-encode base64 (gambar, tabel) dalam satu dokumen. Perubahan kode satu baris saja bisa menghasilkan diff yang besar dan berisik. Meninjau pull request notebook menjadi merepotkan, dan konflik merge sering terjadi.
Sulit Digunakan Ulang
Mengubah notebook menjadi skrip atau modul biasanya berarti menyalin sel ke berkas .py dan merapikan urutan eksekusi secara manual. Notebook itu sendiri tidak bisa langsung diimpor atau dijalankan sebagai program.
Marimo mengambil sikap berbeda pada setiap poin ini. Bagian berikut menjelaskan caranya.
Instalasi dan Langkah Awal
Marimo adalah paket Python standar.
pip install marimo
Pastikan instalasi dan periksa versinya:
marimo --version
Buat notebook baru:
marimo new
Buka notebook yang sudah ada di editor:
marimo edit notebook.py
Editor berjalan di browser, tetapi berkas notebook tersimpan di disk sebagai Python biasa. Jika Anda sudah punya notebook Jupyter, konversikan:
marimo convert oldanalysis.ipynb > newanalysis.py
Untuk menyajikan notebook sebagai aplikasi interaktif yang hanya bisa dibaca, bukan dokumen yang bisa diedit:
marimo run notebook.py
Dalam mode run, sel kode disembunyikan dan hanya UI serta output yang ditampilkan. Berkas yang sama adalah dokumen editor, aplikasi, sekaligus skrip. Tidak ada langkah ekspor terpisah untuk mendapatkan program yang berfungsi.
Format Berkas .py
Notebook Marimo adalah berkas Python biasa. Setiap sel adalah fungsi yang didekorasi dengan @app.cell, dan berkas diakhiri dengan blok runner kecil. Notebook minimal terlihat seperti ini:
import marimo
app = marimo.App()
@app.cell
def ():
import marimo as mo
return (mo,)
@app.cell
def (mo):
x = 21
mo.md(f"x is {x}")
return (x,)
if name == "main":
app.run()
Ada dua hal yang patut diperhatikan. Pertama, ini adalah Python valid yang bisa Anda lint, format dengan alat seperti Black atau Ruff, dan tinjau dalam diff biasa. Perubahan kode tampil sebagai perubahan kode, bukan sebagai gumpalan JSON. Kedua, argumen fungsi dan nilai kembalian bukan boilerplate yang Anda rawat manual. Marimo menghasilkannya dari variabel yang dibaca dan didefinisikan setiap sel. Deklarasi itulah yang menggerakkan model reaktif.
Model Eksekusi Reaktif
Inilah gagasan inti Marimo dan pembeda paling jelas dari Jupyter.
Sel Membentuk Graf Ketergantungan
Marimo menganalisis setiap sel secara statis untuk melihat variabel apa yang didefinisikan dan dirujuknya. Dari situ, Marimo membangun graf berarah tanpa siklus (DAG). Sel yang membaca df bergantung pada sel yang mendefinisikan df. Saat Anda menjalankan atau mengubah sebuah sel, Marimo menjalankan sel itu dan setiap sel di hilirnya, dalam urutan topologis. Sel yang tidak bergantung pada perubahan tersebut dibiarkan.
Efek praktisnya: tidak ada "urutan jalan" yang perlu diingat. Urutan sel dalam berkas tidak menentukan eksekusi; ketergantungan data yang menentukannya. Anda bisa menaruh sel dalam urutan baca apa pun yang membuat dokumen jelas.
Sebuah Variabel Didefinisikan di Tepat Satu Sel
Marimo menegakkan aturan bahwa setiap variabel global hanya boleh diberi nilai di satu sel. Jika dua sel sama-sama mendefinisikan model, Marimo melaporkan error alih-alih diam-diam membiarkan sel terakhir yang menang. Aturan inilah yang membuat DAG terdefinisi dengan baik dan reproducible. Awalnya terasa membatasi, tetapi ini menghapus seluruh kelas kebingungan "sel mana yang mengatur nilai ini?". Saat Anda butuh nama sekali pakai di beberapa tempat, gunakan variabel lokal (beri awalan garis bawah, misalnya tmp) agar tetap privat di selnya.
Tanpa Hidden State
Karena Marimo menurunkan state dari sel yang ada saat ini, menghapus sebuah sel juga menghapus variabel yang didefinisikannya, dan setiap sel yang bergantung langsung diperbarui. Tidak ada nilai sisa dari kode yang sudah tidak ada. Apa yang Anda lihat di berkas adalah apa yang dijalankan. Membuka kembali notebook dan hasil yang tadi Anda peroleh adalah hal yang sama, karena hasilnya adalah fungsi dari kode, bukan dari riwayat klik Anda.
Perbandingan Singkat
Perhatikan dua sel:
@app.cell
def ():
baseprice = 100
return (baseprice,)
@app.cell
def (baseprice):
total = baseprice 1.11
total
return (total,)
Ubah baseprice menjadi 120 di sel pertama, dan sel kedua menghitung ulang dengan sendirinya, menampilkan total yang baru. Di Jupyter Anda harus ingat untuk menjalankan ulang sel kedua. Di Marimo, lupa itu tidak mungkin, karena keterkaitannya menjadi bagian dari graf.
Elemen UI yang Terikat Secara Reaktif
Marimo menyediakan widget interaktif di bawah mo.ui. Perbedaan penting dari widget notebook pada umumnya adalah bahwa .value sebuah elemen UI merupakan variabel biasa dalam graf. Membaca .value di sel lain membuat sel itu bergantung pada widget, sehingga menggeser slider menjalankan ulang persis sel yang memakainya.
@app.cell
def (mo):
npoints = mo.ui.slider(start=10, stop=1000, value=200, step=10, label="Points")
noise = mo.ui.slider(start=0.0, stop=2.0, value=0.3, step=0.05, label="Noise")
mo.hstack([npoints, noise])
return npoints, noise
@app.cell
def (npoints, noise, np):
rng = np.random.defaultrng(0)
x = np.linspace(0, 10, npoints.value)
y = 2.0 x + 1.0 + rng.normal(0, noise.value, size=npoints.value)
return x, y
Saat salah satu slider digeser, sel yang membangun x dan y dijalankan ulang, begitu juga apa pun yang ada di hilirnya. Anda tidak pernah merangkai callback; ketergantungan itu tersirat dari fakta bahwa Anda membaca npoints.value dan noise.value.
Elemen umum lain bekerja dengan cara yang sama:
@app.cell
def (mo):
method = mo.ui.dropdown(
options=["least squares", "ridge"], value="least squares", label="Fit method"
)
label = mo.ui.text(value="experiment-1", label="Run label")
return method, label
@app.cell
def (mo):
upload = mo.ui.file(label="Upload a CSV", filetypes=[".csv"])
upload
return (upload,)
mo.ui.table merender dataframe dengan dukungan seleksi, dan .value-nya menyimpan baris yang dipilih sehingga sel hilir dapat bereaksi terhadap pilihan pengguna.
Menampilkan Output dan Markdown
Ekspresi terakhir dalam sebuah sel menjadi outputnya, mirip Jupyter. Untuk teks dan dokumentasi, mo.md merender Markdown dan mendukung interpolasi f-string, termasuk nilai UI yang hidup:
@app.cell
def (mo, npoints, noise):
mo.md(
f"""
## Dataset sintetis
Dibuat {npoints.value} titik dengan deviasi standar
noise sebesar {noise.value:.2f}.
"""
)
return
Karena sel ini membaca nilai slider, teks yang dirender ikut berubah saat Anda menggeser slider. Anda bisa menyematkan widget langsung di dalam Markdown dengan menginterpolasikan objeknya, yang praktis untuk kontrol inline.
Tata Letak
Marimo menyediakan pembantu tata letak yang menyusun output:
@app.cell
def (mo, npoints, noise, method):
mo.vstack(
[
mo.md("### Kontrol"),
mo.hstack([npoints, noise]),
method,
]
)
return
Gunakan mo.hstack untuk baris, mo.vstack untuk kolom, dan mo.ui.tabs untuk mengelompokkan bagian:
@app.cell
def (mo, chart, summarytable):
mo.ui.tabs({"Chart": chart, "Data": summarytable})
return
Contoh Utuh: Fit Linear Interaktif
Menyatukan semua bagian, berikut demo model kecil. Slider mengontrol data, dropdown memilih metode fit, dan chart diperbarui secara reaktif. Setiap sel di bawah akan menjadi sebuah fungsi @app.cell dalam berkas notebook; di sini ditampilkan sebagai isi sel agar mudah dibaca.
# imports
import marimo as mo
import numpy as np
import polars as pl
import altair as alt
# controls
npoints = mo.ui.slider(10, 1000, value=200, step=10, label="Points")
noise = mo.ui.slider(0.0, 2.0, value=0.3, step=0.05, label="Noise")
method = mo.ui.dropdown(["least squares", "ridge"], value="least squares", label="Method")
mo.hstack([npoints, noise, method])
# data generation
rng = np.random.defaultrng(0)
x = np.linspace(0, 10, npoints.value)
y = 2.0 x + 1.0 + rng.normal(0, noise.value, size=npoints.value)
data = pl.DataFrame({"x": x, "y": y})
# fit the model
if method.value == "ridge":
lam = 1.0
X = np.vstack([x, np.oneslike(x)]).T
coef = np.linalg.solve(X.T @ X + lam np.eye(2), X.T @ y)
else:
coef = np.polyfit(x, y, 1)
slope, intercept = float(coef[0]), float(coef[1])
yfit = slope x + intercept
# chart
fitted = data.withcolumns(pl.Series("yfit", yfit))
points = alt.Chart(fitted.topandas()).markcircle(opacity=0.5).encode(x="x", y="y")
line = alt.Chart(fitted.topandas()).markline(color="red").encode(x="x", y="yfit")
chart = mo.ui.altairchart(points + line)
chart
# live summary
mo.md(
f"""
Metode: {method.value}
Slope: {slope:.3f} Intercept: {intercept:.3f}
Sampel: {npoints.value}
"""
)
Geser slider atau ubah dropdown, dan data, fit, chart, serta ringkasan dihitung ulang dalam urutan ketergantungan, tanpa rerun manual. Notebook yang sama juga merupakan program Python yang berfungsi, seperti ditunjukkan bagian berikut.
Menjalankan sebagai Skrip
Karena berkasnya adalah Python biasa dengan titik masuk app.run(), Anda bisa mengeksekusinya langsung:
python notebook.py
Ini menjalankan setiap sel sekali, dari puncak graf ke bawah, tanpa editor. Berguna untuk batch job, tugas cron, atau CI. Elemen UI mengambil nilai default saat tidak ada sesi interaktif, jadi rancanglah default yang masuk akal untuk eksekusi tanpa pengawasan.
Argumen Command-Line dan Parameterisasi
Agar skrip bisa dikonfigurasi, baca argumen CLI dengan mo.cliargs:
@app.cell
def (mo):
args = mo.cliargs()
seed = int(args.get("seed", 0))
outpath = args.get("out", "results.csv")
return seed, outpath
Berikan nilai setelah pemisah --:
python notebook.py -- --seed 42 --out run42.csv
Argumen yang sama tersedia saat menyajikan dengan marimo run, sehingga satu notebook bisa diparameterisasi secara konsisten lintas mode skrip dan aplikasi.
Menjalankan sebagai Aplikasi Web dan Deploy
marimo run notebook.py menyajikan notebook sebagai aplikasi interaktif dengan kode disembunyikan. Anda bisa meng-host-nya seperti layanan web mana pun:
marimo run notebook.py --host 0.0.0.0 --port 8080
Untuk deployment, jalankan perintah itu di balik process manager atau di dalam container. Marimo juga merupakan proses Python biasa, jadi opsi umum (unit systemd, image Docker, worker platform-as-a-service) semuanya berlaku.
Aplikasi Khusus Browser dengan WASM
Marimo bisa mengekspor notebook ke berkas HTML mandiri yang berjalan sepenuhnya di browser melalui WebAssembly, tanpa server:
marimo export html-wasm notebook.py -o site/index.html --mode run
Host berkas hasilnya di static host mana pun. Gunakan --mode edit untuk mengirim notebook WASM yang bisa diedit alih-alih aplikasi hanya-baca. Ini cocok untuk situs dokumentasi, demo, dan materi pengajaran ketika Anda tidak ingin menjalankan backend.
Sel SQL dan Integrasi DataFrame
Marimo memiliki dukungan kelas satu untuk dataframe dan SQL. Anda bisa mencampur Polars, pandas, dan DuckDB dalam satu notebook, dan sel SQL dapat mengkueri dataframe secara langsung:
@app.cell
def (mo, data):
result = mo.sql(
"""
SELECT
round(x) AS xbucket,
avg(y) AS meany,
count() AS n
FROM data
GROUP BY xbucket
ORDER BY xbucket
"""
)
return (result,)
Kueri berjalan melalui DuckDB, merujuk dataframe data berdasarkan nama, dan mengembalikan dataframe. Karena sel SQL membaca data, ia menjadi bagian dari graf reaktif yang sama: regenerasi data dan kueri akan dijalankan ulang. Di editor, Marimo juga menyediakan tipe sel SQL sehingga Anda bisa menulis kueri tanpa pembungkus mo.sql(...).
Cache Sel Mahal dan Berhenti Lebih Awal
Untuk komputasi lambat, mo.cache melakukan memoisasi sebuah fungsi berdasarkan inputnya, sehingga rerun yang dipicu perubahan tak terkait tidak menghitungnya ulang:
@app.cell
def (mo, np):
@mo.cache
def expensivefit(seed: int, size: int):
rng = np.random.defaultrng(seed)
sample = rng.normal(size=size)
return float(sample.mean()), float(sample.std())
return (expensivefit,)
Untuk hasil yang harus bertahan setelah kernel di-restart, gunakan persistent cache milik Marimo, yang menyimpan entri di disk:
@app.cell
def (mo, np):
with mo.persistentcache(name="sampling"):
big = np.random.defaultrng(0).normal(size=10000000)
bigmean = float(big.mean())
return (bigmean,)
Untuk menghentikan sel lebih awal, mo.stop menghentikan eksekusi (dan sel di hilir outputnya) ketika sebuah kondisi terpenuhi. Ini berguna untuk berjaga terhadap input yang hilang:
@app.cell
def (mo, upload):
mo.stop(upload.value is None, mo.md("Upload sebuah CSV untuk melanjutkan."))
rawbytes = upload.value[0].contents
return (rawbytes,)
Ketika berkas tidak ada, sel menampilkan pesan dan berhenti, dan sel yang bergantung tidak berjalan dengan data yang tidak valid.
Menguji Notebook dengan pytest
Karena notebook adalah Python yang bisa diimpor, Anda bisa mengujinya. Definisikan fungsi biasa di dalam sel dan impor di modul tes, atau jalankan sel langsung. Sel tes yang nama fungsinya diawali test juga dikumpulkan oleh pytest saat Anda mengarahkannya ke berkas tersebut:
@app.cell
def (expensivefit):
def testfitisdeterministic():
assert expensivefit(1, 100) == expensivefit(1, 100)
return (testfitisdeterministic,)
pytest notebook.py
Artinya notebook yang sama bisa berfungsi sebagai eksplorasi, aplikasi, sekaligus tempat menyimpan tes regresi di dekat kode yang dicakupnya.
Praktik Terbaik
Jaga satu tanggung jawab per sel. Sel kecil membuat graf ketergantungan jelas dan rerun murah, serta enak dibaca dalam diff.
Pilih nama global yang deskriptif untuk nilai yang melintasi sel, dan lokal berawalan garis bawah untuk variabel sementara. Ini menjaga aturan definisi tunggal agar tidak mengganggu Anda.
Pilih default yang masuk akal untuk setiap elemen UI. Default itulah yang dipakai mode skrip dan eksekusi tanpa pengawasan, jadi harus menghasilkan hasil yang valid dengan sendirinya.
Lakukan cache pada batas fungsi, bukan di sekeliling blok besar. mo.cache mengunci berdasarkan argumen, jadi berikan input secara eksplisit alih-alih mengandalkan closure atas global notebook.
Gunakan mo.stop untuk menjaga sel yang bergantung pada upload, panggilan jaringan, atau input lain yang mungkin tidak ada, agar graf gagal dengan anggun alih-alih menghasilkan sampah.
Commit berkas .py dan tinjau seperti kode. Karena diff-nya Python sungguhan, code review dan CI bekerja sebagaimana pada bagian lain proyek Anda.
Marimo vs. Jupyter Sekilas
Penyimpanan: Marimo memakai berkas .py biasa; Jupyter memakai JSON .ipynb dengan output tertanam.
Eksekusi: Marimo menjalankan ulang sel bergantung secara otomatis melalui graf ketergantungan; Jupyter menjalankan sel secara manual dalam urutan klik apa pun.
State: Marimo menurunkan state dari sel yang ada dan menghapus variabel saat sebuah sel dihapus; Jupyter menyimpan hidden state dari sel yang dihapus atau tidak berurutan.
Penggunaan ulang: Notebook Marimo berjalan sebagai skrip, disajikan sebagai aplikasi, dan diimpor sebagai modul; notebook Jupyter perlu dikonversi untuk menjadi program.
Version control: Diff Marimo adalah diff kode biasa; diff Jupyter adalah JSON yang berisik.
Interaktivitas: Pada Marimo, nilai UI adalah variabel graf, sehingga widget memperbarui output tanpa callback; widget Jupyter butuh observer eksplisit atau rerun manual.
Ini bukan klaim bahwa Marimo menggantikan Jupyter untuk setiap tugas. Jupyter punya ekosistem yang luas dan banyak pengguna lebih suka eksekusi bebasnya untuk coretan cepat. Pertukaran yang diambil Marimo adalah menerima beberapa aturan, terutama definisi tunggal dan reaktivitas, sebagai ganti reproducibility dan penggunaan ulang.
Kesimpulan dan Poin Penting
Marimo membingkai ulang notebook sebagai program reaktif yang disimpan sebagai Python biasa. Graf ketergantungan menghapus eksekusi tidak berurutan, aturan definisi tunggal dan state turunan menghapus hidden state, dan format .py membuat notebook bisa ditinjau, diuji, serta dijalankan sebagai skrip atau aplikasi.
Poin penting:
- Pasang dengan
pip install marimo; edit denganmarimo edit, sajikan denganmarimo run, konversi denganmarimo convert. - Sel membentuk DAG; mengubah sebuah nilai menjalankan ulang hanya sel yang bergantung, dan setiap variabel didefinisikan di tepat satu sel.
.valueelemen UI adalah variabel graf, sehingga widget menggerakkan pembaruan tanpa callback.- Notebook adalah berkas
.pysungguhan: ramah git, bisa diimpor, bisa dijalankan denganpython notebook.py, dan bisa diparameterisasi viamo.cliargs. - Gunakan
mo.cachedan persistent cache untuk pekerjaan mahal,mo.stopuntuk menjaga sel,mo.sqluntuk kueri berbasis DuckDB, danpytestuntuk pengujian. - Ekspor ke WASM dengan
marimo export html-wasmuntuk deployment khusus browser.
Jika tim Anda pernah terbakar oleh notebook yang berfungsi di satu mesin namun gagal di mesin lain, model Marimo layak dicoba secara serius pada proyek nyata.