Marimo : le notebook Python réactif qui ringardise Jupyter

Marimo : le notebook Python réactif qui ringardise Jupyter

De quoi avez-vous besoin

Version de Python

3.x

Packages

  • {"nom":"marimo","version":">=0.9.0"}
  • {"nom":"pandas","version":">=2.0"}
  • {"nom":"matplotlib","version":">=3.7"}

Difficulté

Débutant

Tu en as assez de relancer tes cellules Jupyter une par une, de traquer l'ordre d'exécution et de redécouvrir, en rouvrant ton notebook, qu'une variable fantôme traîne encore en mémoire ? Marimo est un notebook Python nouvelle génération qui résout ces problèmes à la racine. Son idée maîtresse : la réactivité. Chaque cellule est une fonction de ses dépendances, et dès que tu modifies une valeur, tout ce qui en dépend se recalcule automatiquement, comme dans un tableur.

Le résultat, c'est un notebook où l'état caché disparaît, où le code et la sortie sont toujours synchronisés, et qui se transforme sans effort en application web interactive ou en artefact à partager. Dans ce tutoriel, on explore Marimo de l'installation à l'export, en passant par les widgets, le state, les graphiques et le SQL.

Ce tutoriel s'adresse à tous ceux qui touchent à Python et à la donnée : les débutants curieux de découvrir une alternative moderne à Jupyter comme les habitués lassés de ses limites. Aucun prérequis au-delà d'une installation de Python récente et d'une envie d'essayer.

Le problème avec Jupyter

Jupyter est formidable pour l'exploration, mais son modèle d'exécution manuelle engendre des bugs sournois qui coûtent des heures. Concrètement :

  • L'état caché : une cellule exécutée puis supprimée laisse ses variables en mémoire, invisibles à l'écran.
  • L'ordre d'exécution : si tu relances la cellule 2 après la 5, le notebook peut afficher un résultat incohérent avec le code affiché à l'écran.
  • La non-reproductibilité : rouvrir un notebook et tout relancer ne garantit pas d'obtenir les mêmes résultats.
  • Le partage laborieux : transformer un notebook en app ou en rapport propre demande des outils externes (Voila, nbconvert).

Marimo élimine ces problèmes en rendant l'exécution déclarative : tu écris ce que tu veux calculer, et le moteur se charge de calculer dans le bon ordre, à chaque fois.

Installation et prise en main

Marimo s'installe en une commande et se lance dans ton navigateur. Aucune extension, aucun serveur à configurer.

bash
# Depuis un terminal
pip install marimo

# Lancer l'éditeur (ouvre le navigateur)
marimo edit

# Lancer le tutoriel interactif intégré
marimo tutorial intro

# Vérifier la version
marimo --version

La commande marimo edit ouvre l'éditeur ; marimo tutorial intro lance un didacticiel interactif qui te fait manipuler les concepts de base en dix minutes. Si tu viens de Jupyter, tu peux aussi importer tes anciens notebooks avec marimo convert notebook.ipynb -o notebook.py.

Ton premier notebook réactif

Une cellule Marimo est un simple bout de Python. Rien de spécial à la création : la magie opère quand une variable change.

python
import marimo as mo

# Une cellule Marimo est un simple morceau de Python.
# La différence avec Jupyter : si cette variable change,
# toutes les cellules qui en dépendent se recalculent.
nom = "Léa"
age = 28

mo.md(f"Bonjour {nom}, tu as {age} ans.")

Modifie age en 30 dans la première cellule : la seconde, qui affiche le message, se recalcule instantanément. Aucun bouton à cliquer, aucun ordre à respecter. C'est le cœur du modèle réactif.

La réactivité en action

Les widgets marimo (préfixe mo.ui) sont la façon naturelle d'introduire de l'interactivité. Un curseur, par exemple, est à la fois un objet Python et un élément d'interface.

python
import marimo as mo

# Un curseur interactif : mo.ui.slider renvoie un widget.
# Affiche le widget en le plaçant seul dans sa cellule.
slider = mo.ui.slider(start=1, stop=100, step=1, value=42, label="Nombre")
slider

Dans la cellule suivante, on consomme slider.value. Cette cellule devient alors dépendante du curseur : dès que tu le déplaces, elle se réévalue.

python
# Cette cellule dépend de slider.value : elle se met à jour
# automatiquement dès que tu bouges le curseur. Aucun bouton "Run".
carre = slider.value ** 2
mo.md(f"Le carré de {slider.value} vaut **{carre}**.")

Retiens la règle d'or de Marimo : une variable ne peut être définie qu'à un seul endroit du notebook. Pas de réaffectation de la même variable dans deux cellules. Si tu as besoin de transformer une valeur, utilise une nouvelle variable. Cette contrainte, parfois déroutante au début, est précisément ce qui garantit la cohérence de l'ensemble.

Construire des widgets interactifs

La bibliothèque mo.ui fournit toute une panoplie de widgets prêts à l'emploi : curseurs, cases à cocher, champs texte, menus déroulants, boutons, et bien d'autres. Chacun expose sa valeur via l'attribut .value.

python
import marimo as mo

slider = mo.ui.slider(0, 10, value=3, label="Niveau")
checkbox = mo.ui.checkbox(value=True, label="Activer")
texte = mo.ui.text(value="", placeholder="Ton nom")
choix = mo.ui.dropdown(["rouge", "vert", "bleu"], value="vert", label="Couleur")
bouton = mo.ui.button(label="Valider")

# mo.hstack aligne les widgets horizontalement
mo.hstack([slider, checkbox, choix])
texte
bouton

Les widgets se combinent pour créer de vrais mini-formulaires. Le bouton, lui, possède une propriété .value qui passe à True à chaque clic : on s'en sert pour déclencher des actions.

python
# On lit la valeur d'un widget via l'attribut .value
if bouton.value:
    mo.md(f"Bonjour **{texte.value or 'anonyme'}** ! "
          f"Niveau {slider.value}, couleur {choix.value}.")
else:
    mo.md("Clique sur Valider pour afficher le message.")

Composer l'interface avec les layouts

En plus des widgets, marimo propose des conteneurs pour organiser l'espace : piles horizontales ou verticales, accordéons, onglets, barres latérales. Ces éléments transforment un notebook en véritable petite application.

python
import marimo as mo

# Accordéon repliable
mo.accordion({
    "Introduction": mo.md("Un notebook réactif, reproductible et partageable."),
    "Installation": mo.md("pip install marimo"),
    "Exemples": mo.md("Slider, graphique, table, formulaire..."),
})

# Onglets
mo.tabs({
    "Données": mo.md("Chargement des données ici."),
    "Analyse": mo.md("Transformations et statistiques."),
    "Sortie": mo.md("Graphiques et tableaux finaux."),
})

mo.hstack et mo.vstack alignent les éléments ; mo.accordion regroupe des blocs repliables ; mo.tabs crée des onglets. Tu peux imbriquer ces conteneurs à volonté pour bâtir une mise en page complète, du tableau de bord au formulaire de configuration.

Le state : garder une mémoire

Dans un notebook réactif, les valeurs sont recalculées, jamais mutées sur place. Pour conserver un état qui évolue au fil des interactions (un compteur, une progression, une configuration), Marimo fournit mo.state.

python
import marimo as mo

# mo.state crée un état persistant : (fonction de lecture, fonction d'écriture)
get_count, set_count = mo.state(0)

def incrementer(_):
    set_count(get_count() + 1)

bouton = mo.ui.button(label="Incrémenter", on_click=incrementer)

mo.hstack([bouton, mo.md(f"Compteur : {get_count()}")])

mo.state(0) renvoie deux fonctions : get_count pour lire la valeur courante et set_count pour l'écrire. Le bouton appelle incrementer à chaque clic via son paramètre on_click. Le compteur persiste entre les interactions, chose impossible à exprimer avec de simples variables réactives.

DataFrames et visualisations

Marimo s'intègre naturellement à l'écosystème pandas et Altair. Le tableau interactif mo.ui.table ajoute le tri, le filtrage et la recherche sans code supplémentaire, et les graphiques Altair deviennent réactifs à la moindre modification des données.

python
import marimo as mo
import pandas as pd
import altair as alt

df = pd.DataFrame({
    "mois": ["Jan", "Fév", "Mar", "Avr", "Mai", "Juin"],
    "ventes": [120, 145, 132, 178, 210, 190],
})

# Un tableau interactif : tri, filtrage, recherche intégrés
mo.ui.table(df)

# Un graphique Altair réactif
chart = (
    alt.Chart(df)
    .mark_line(point=True)
    .encode(x="mois", y="ventes")
    .properties(title="Ventes mensuelles")
)
mo.ui.altair_chart(chart)

Comme tout le reste, ces sorties sont réactives : change une valeur dans le DataFrame d'origine et le tableau comme le graphique se mettent à jour. Pour des visualisations plus riches, marimo supporte aussi Plotly, Matplotlib et toute la famille des bibliothèques de tracé.

SQL directement dans ton notebook

Si tes données vivent dans des DataFrames ou une base, mo.sql te permet d'écrire des requêtes SQL directement dans une cellule. Le résultat s'affiche comme un tableau et peut être réutilisé dans d'autres cellules.

python
import marimo as mo
import pandas as pd

# Une table à interroger
df = pd.DataFrame({
    "produit": ["clavier", "souris", "écran", "webcam", "casque"],
    "prix": [45, 25, 180, 60, 90],
    "stock": [12, 30, 5, 8, 0],
})

# mo.sql exécute du SQL directement sur tes DataFrames.
# Le résultat est affiché comme un tableau, et tu peux
# référencer la sortie dans d'autres cellules.
mo.sql(
    f"SELECT produit, prix FROM df WHERE stock > 0 ORDER BY prix DESC",
    output=True,
)

C'est un confort énorme quand tu manipules de la donnée tabulaire : tu restes dans le langage que tu maîtrises pour filtrer, agréger et joindre, sans réécrire des chaînes de pandas parfois illisibles.

Gérer les dépendances

Contrairement à Jupyter qui partage l'environnement Python global, Marimo isole les dépendances par notebook. Chaque notebook déclare ses paquets, ce qui le rend reproductible et portable : celui qui ouvre ton fichier obtient exactement le même environnement.

python
# Marimo gère ses propres dépendances, isolées de ton Python système.
# Pour ajouter un paquet, trois options :

# 1. La commande magique pip (en cellule)
#    %pip install requests

# 2. Le gestionnaire de paquets intégré (icône "Packages" en bas à gauche)

# 3. Un fichier pyproject.toml à la racine du notebook, exemple :
#    [project]
#    dependencies = ["requests", "pandas", "altair"]

import requests
r = requests.get("https://api.github.com/repos/marimo-team/marimo")
print("Étoiles GitHub de marimo :", r.json()["stargazers_count"])

Cette isolation évite le cauchemar classique des conflits de versions entre projets. Au premier lancement, Marimo installe automatiquement les paquets manquants dans un environnement dédié, puis les réutilise d'une session à l'autre.

Exporter et partager

Un notebook Marimo n'est pas condamné à rester un fichier .py. Il s'exporte en HTML autonome pour le partage, en notebook Jupyter pour la compatibilité, ou se lance comme une application web à part entière.

bash
# Exporter le notebook en HTML autonome (rapport, partage)
marimo export html notebook.py -o rapport.html

# Exporter en script Python pur (réutilisable hors notebook)
marimo export ipynb notebook.py -o notebook.ipynb   # vers Jupyter
marimo export script notebook.py -o script.py       # vers .py

# Lancer un notebook comme une application web (sans éditeur)
marimo run notebook.py

# Déployer gratuitement sur le cloud Marimo
marimo deploy notebook.py

marimo run transforme ton notebook en app web : les widgets restent interactifs, mais l'éditeur disparaît, ce qui donne un outil propre à mettre entre les mains d'un collègue ou d'un client. marimo deploy pousse le tout sur le cloud Marimo avec une URL publique, gratuitement pour les projets ouverts.

Les pièges à connaître

  • Variable définie deux fois : Marimo refuse les définitions dupliquées. Si tu veux itérer sur une valeur, crée une nouvelle variable plutôt que de réaffecter.
  • Oubli du .value : un widget n'est pas sa valeur. Écris slider.value, pas slider, dans tes calculs.
  • Cellules trop grosses : découpe tes cellules en unités logiques. La réactivité ne recalculera que ce qui est nécessaire, et le notebook restera lisible.
  • Dépendance circulaire : si deux cellules dépendent l'une de l'autre, Marimo le signale. Restructure pour briser le cycle.
  • Performance : une cellule coûteuse (chargement d'un gros fichier) se réexécute à chaque changement de ses dépendances. Utilise mo.cache pour mémoïser les calculs lourds.

Reproductibilité : un état qui ne ment pas

Le plus grand bénéfice de la réactivité, c'est la reproductibilité. Dans Jupyter, l'état d'un notebook dépend de l'ordre dans lequel tu as cliqué sur les cellules ; rouvrir le fichier ne te dit rien de cet historique, et relancer les cellules dans un autre ordre peut produire un résultat différent. Dans Marimo, l'état est entièrement déterminé par le code : exécuter le notebook de haut en bas donne toujours le même résultat.

Cette propriété change la donne pour le travail en équipe. Un notebook Marimo est une description déclarative de ton analyse : pas d'étape cachée, pas de variable fantôme, pas de cellule oubliée. Si deux personnes ouvrent le même fichier avec le même environnement, elles voient exactement la même chose. C'est aussi une exigence de plus en plus fréquente dans les contextes réglementés, où l'on doit pouvoir prouver comment un résultat a été obtenu.

Les cellules sont des fonctions

Pour tirer le meilleur parti de Marimo, pense à chaque cellule comme à une fonction : elle lit des variables en entrée et produit une sortie. Les fonctions Python classiques restent évidemment disponibles, et c'est une bonne pratique de factoriser la logique réutilisable dans des fonctions, appelées ensuite par les cellules réactives.

python
import marimo as mo

# La logique métier vit dans une fonction classique, testable et réutilisable
def prix_ttc(prix_ht, taux=0.2):
    return round(prix_ht * (1 + taux), 2)

montant = mo.ui.number(value=100, label="Montant HT")
montant

# La cellule réactive ne fait qu'afficher le résultat de la fonction
mo.md(f"Montant TTC : **{prix_ttc(montant.value)}** euros")

Cette séparation entre logique (les fonctions) et interface (les cellules) rend ton notebook plus lisible et plus facile à maintenir. Les fonctions peuvent être extraites dans un module séparé et testées unitairement, tandis que le notebook se concentre sur l'exploration et la présentation.

mo.md et le Markdown enrichi

Pour composer du texte, des titres, des listes ou des citations, mo.md rend du Markdown enrichi, y compris du LaTeX pour les mathématiques (entre deux paires de dollars). C'est ton outil de base pour structurer la narration d'un notebook et le transformer en un véritable rapport.

python
import marimo as mo

# mo.md rend du Markdown enrichi (titres, listes, citations)
mo.md("# Bilan trimestriel")

mo.md("Un **chiffre clé** : `revenu = prix * volume`")

mo.md("- Un point à retenir
- Un second point")

Contrairement aux cellules Markdown de Jupyter qui sont statiques, le Markdown de Marimo est réactif : tu peux y interpoler la valeur d'un widget ou d'un calcul, et le texte se met à jour en direct. C'est ce qui permet de construire des tableaux de bord narratifs où le texte reflète toujours les données affichées.

Exécuter un notebook comme un script

Un notebook Marimo est un fichier Python ordinaire, pas un format propriétaire. Conséquence pratique : tu peux l'exécuter de haut en bas comme n'importe quel programme, ce qui en fait un excellent support pour l'automatisation.

bash
# Un notebook Marimo est un programme Python valide :
# on peut l'exécuter de haut en bas, sans interface.
python notebook.py

# marimo run le sert comme application web (widgets interactifs)
marimo run notebook.py

Ce double visage est rare : le même fichier sert à la fois de cahier d'exploration interactif et de job reproductible que tu peux glisser dans un pipeline ou une tâche planifiée. Pas de conversion, pas de dérive entre ce que tu explores et ce que tu exécutes en production.

Marimo vs Jupyter : le bilan

Pour récapituler ce qui sépare les deux outils, voici une comparaison point par point :

  • Exécution : Jupyter exécute manuellement, dans l'ordre de ton choix ; Marimo exécute automatiquement, dans un ordre déduit des dépendances.
  • État : Jupyter garde un état caché et fragile ; Marimo déduit l'état du code, donc toujours cohérent.
  • Reproductibilité : Jupyter dépend de l'historique d'exécution ; Marimo la garantit par construction.
  • Interactivité : Jupyter s'appuie sur des extensions (ipywidgets) ; Marimo l'a nativement via mo.ui.
  • Partage : Jupyter passe par nbconvert ou Voila ; Marimo exporte en HTML et se lance en app web d'un coup.
  • Dépendances : Jupyter partage l'environnement Python global ; Marimo isole les paquets par notebook.

Les formulaires avec mo.ui.form

Pour collecter plusieurs champs et les valider ensemble, mo.ui.form regroupe des widgets dans un formulaire qui se soumet d'un coup. C'est la brique qui permet de construire de vrais écrans de saisie.

python
import marimo as mo

# Un formulaire regroupe plusieurs champs et se valide d'un seul clic
form = mo.ui.form(
    nom=mo.ui.text(label="Nom"),
    age=mo.ui.number(start=0, stop=120, label="Âge"),
)
form

# À la soumission, form.value contient un dict des valeurs saisies
if form.value is not None:
    mo.md(f"Enregistré : **{form.value['nom']}**, {form.value['age']} ans")

La valeur n'est disponible qu'après soumission, ce qui évite les réactions prématurées à chaque frappe. C'est exactement le comportement attendu d'un formulaire classique, et cela rend Marimo adapté à la saisie de paramètres ou de données structurées.

mo.cache pour les calculs lourds

La réactivité a un coût : une cellule qui charge un gros fichier se réexécute dès qu'une de ses dépendances change. Pour les opérations coûteuses et déterministes, mo.cache mémoïse le résultat : il n'est recalculé que si les arguments changent.

python
import marimo as mo

@mo.cache
def charger_donnees(chemin):
    # Simulation d'un chargement long (fichier volumineux, requête API...)
    import time
    time.sleep(2)
    return {"lignes": 1000}

donnees = charger_donnees("donnees.csv")
mo.md(f"{donnees['lignes']} lignes chargées")

Tant que le chemin passé en argument ne change pas, la fonction n'est pas réexécutée : tu modifies une autre cellule sans subir le délai de chargement. C'est le réflexe à adopter dès qu'un notebook commence à devenir lent.

Les composants avancés

Au-delà des widgets de base, Marimo embarque des composants prêts pour la donnée et le multimédia :

  • mo.ui.table et mo.ui.dataframe : tableaux interactifs avec tri, filtrage et recherche.
  • mo.ui.altair_chart, mo.ui.plotly : graphiques réactifs reliés à tes données.
  • mo.ui.file : upload de fichiers directement depuis le navigateur.
  • mo.ui.batch : un seul widget pour manipuler plusieurs valeurs d'un coup.
  • mo.ui.anywidget : intégrer n'importe quel widget JavaScript personnalisé.

Cette richesse fait de Marimo bien plus qu'un carnet de notes : c'est un véritable atelier de construction d'interfaces, où la frontière entre analyse et application s'estompe naturellement.

Versionner et collaborer avec Git

Parce qu'un notebook Marimo est un simple fichier .py, il se prête naturellement au versionnement. Là où les notebooks Jupyter (.ipynb) produisent des diffs illisibles mêlant code, sorties et métadonnées, un notebook Marimo génère un diff propre, limité à ce qui a réellement changé.

bash
# Un notebook Marimo est un fichier .py : le diff Git est lisible
git add notebook.py
git commit -m "Ajout de l'analyse des ventes trimestrielles"

# Revenir en arrière ou comparer deux versions se fait naturellement
git diff HEAD~1 notebook.py

Cette propriété facilite la revue de code, le travail à plusieurs sur une même analyse, et l'intégration de tes notebooks dans des pipelines de CI. C'est un argument de poids pour les équipes data qui souffrent de la friction du format .ipynb dans leurs dépôts.

Autre conséquence : pas de sorties stockées dans le fichier. Le notebook ne contient que le code et la structure, jamais les résultats, ce qui évite les diffs gonflés par des images ou des tableaux régénérés à chaque exécution.

Quand privilégier Marimo ?

Marimo brille particulièrement dans quelques situations précises, où il fait gagner un temps considérable :

  • Tu explores des données et veux des widgets interactifs sans rien configurer.
  • Tu dois partager une analyse sous forme d'application web sans réécrire de code.
  • Ton travail doit être reproductible et versionné proprement avec Git.
  • Tu en as assez des bugs d'état caché qui plombent tes notebooks Jupyter.
  • Tu veux un seul fichier qui serve à la fois de cahier d'exploration et de job exécutable.

Conclusion

Marimo reprend ce que Jupyter a de meilleur, l'exploration interactive, et y ajoute ce qui lui manquait : la réactivité, la reproductibilité et une vraie voie de passage vers l'application. Le modèle mental change, celui du tableur plutôt que du script, mais une fois la règle de la définition unique intégrée, on ne veut plus revenir en arrière.

Pour aller plus loin, explore les composants avancés (formulaires, routes multi-pages avec marimo islands), le cache, et les notebooks exécutés comme des pipelines avec marimo run en mode non interactif. La documentation officielle est remarquablement claire et bourrée d'exemples exécutables.

Sources