Python Notebooks in Production: How marimo Solves Jupyter’s Biggest Problems for Software Engineers

Stop copy-pasting between notebooks and .py files. marimo brings you Git-friendly version control, proper debugging, code reusability, and executable scripts—all in a notebook interface. No complex workflows or bundling required.

DATA ENGINEER
BEGINNER
TECHNICAL UPSKILL
BEST PRACTICES
LATEST TRENDS
Author

Joseph Machado

Published

January 18, 2026

Keywords

python notebooks production, jupyter notebook alternatives, marimo python, jupyter problems software engineering, python notebook version control, production notebook development, jupyter vs marimo, python notebook best practices, jupyter notebook git diff, notebook code reusability, python notebook debugging, jupyter notebook testing, reactive python notebooks, notebook dependency tracking, jupyter to python conversion, notebook software engineering, how to version control jupyter notebooks, debug python notebooks, import functions from jupyter notebook, python notebook sql integration, no code data transformation python

Notebooks are hard to maintain

Notebooks are helpful when checking logic and validating data. But they are not designed with software engineering best practices and are hard to maintain.

If you feel that notebooks:

Do the opposite of what a good tool should: they do not make it hard enough do the wrong thing (copypaste code, no version control, directly attaching prod resources to personal accounts)

Promote non-reusable code, bad habits, and are a security nightmare (due to the ipynb containing code and data)

Make it extremely hard to debug or test

This post is for you. Imagine being able to *explore data as you develop your production code. No need for complex workflows, copy-pasting code between notebooks and Python files, no complex bundling of notebooks, etc.

By the end of this post, you will know how to leverage the interactive & exploratory capabilities of Jupyter notebooks, along with SWE best practices, using marimo.

prerequistes

  1. uv

Create a repo and install the necessary libraries as shown below.

uv init marimo_notebooks 
cd marimo_notebooks
uv add jupyterlab marimo pandas polars pytest requests sqlalchemy

Note We will use uv run .. for the commands in the images, this will use the uv environment to run the commands.

In your repo, create a Python file.

touch marimo_example.py

Copy-paste this into the Python file.

import marimo

__generated_with = "0.19.4"
app = marimo.App()


@app.cell
def _():
    a = 10
    return (a,)


@app.cell
def _(a):
    a + 20
    return


@app.cell
def _(a):
    b = a + 30
    return (b,)


@app.function
def some_marimo_nb_function(a: int, b: int, c: int) -> int:
    return a + b + c


@app.cell
def _(b):
    b
    return


@app.cell
def _():
    b = 10
    return (b,)


@app.cell
def _():
    def non_importable_marimo_nb_function(a: int, b: int, c: int) -> int:
        return a + b + c

    print("""having a function and some executable code makes a function
    non importable""")
    return


@app.cell
def _(a):
    some_marimo_nb_function(a, 1, 2)
    return


@app.function
def test_some_marimo_nb_function():
    print("Running Tests...")
    assert some_marimo_nb_function(1, 2, 3) == 6


@app.cell
def _():
    print("standard python run...")
    return


@app.cell
def _(g):
    for j in range(10):
        if j == 5:
            print(g)
            print(j)
    return


@app.cell
def _():
    for i in range(10):
        if i == 8:
            breakpoint()
            print(i)
    return


@app.cell
def _():
    import sqlalchemy

    # Create an in-memory SQLite database with SQLAlchemy
    sqlite_engine = sqlalchemy.create_engine("sqlite:///:memory:")
    return (sqlite_engine,)


@app.cell
def _():
    import marimo as mo

    return (mo,)


@app.cell
def _(mo, sqlite_engine):
    _df = mo.sql(
        """
        create TABLE employee (id int, name VARCHAR)
        """,
        engine=sqlite_engine,
    )
    return


@app.cell
def _(mo, sqlite_engine):
    _df = mo.sql(
        """
        INSERT into employee values (1, "john"), (2, "doe")
        """,
        engine=sqlite_engine,
    )
    return


@app.cell
def _(mo, sqlite_engine):
    _df = mo.sql(
        """
        SELECT * FROM employee
        """,
        engine=sqlite_engine,
    )
    return


@app.cell
def _():
    import polars as pl
    import requests

    url = "https://raw.githubusercontent.com/vega/vega-datasets/master/data/cars.json"
    response = requests.get(url)
    df = pl.read_json(response.content)

    return (df,)


@app.cell
def _(df, mo):
    transformed_df = mo.ui.dataframe(df)
    transformed_df
    return (transformed_df,)


@app.cell
def _(transformed_df):
    transformed_df
    return


@app.cell
def _():
    return


if __name__ == "__main__":
    app.run()

Run marimo on this file.

uv run marimo edit marimo_example.py

Combine Software Engineering Best Practices with Exploratory Pipeline Development

By using marimo, you can get the best of both worlds:

  1. Exploratory data pipeline development, where you can see the shape of your data as you develop your pipeline
  2. Code saved as Python files enabling: tests, debugging, importable functions, version control, git diffs, etc

Version Control

Jupyter notebooks are json files containing both the code and any output. It is extremely hard for a developer to identify code changes.

Marimo saves its notebooks as Python files. Each marimo cell block is a function, and it does not save its output, making it easy to identify code changes.

Marimo python

Marimo python

Git diffs are simple.

git diff

git diff

Code Reusability

To productionize code in a Jupyter notebook, you will need to copy and paste it into a Python file. Manual copy-paste can lead to missed logic, incorrect ordering, and other issues.

With marimo, you can create functions in your notebook and use them in other Python scripts.

This is possible since Marimo saves notebooks as Python files.

Reusable functions

Reusable functions

Write Tests as You Would for a Python File

As marimo notebooks are just Python files, you can write tests for them. In addition, you can also add tests directly in the notebook and run them with pytest.

Run tests

Run tests

Debugging Code in Marimo Notebook

You can set a debug point in your notebook using breakpoint(). When the logic gets to this point, a debug session is started. You can inspect variables and debug your code.

Debug

Debug

You can also start a debugger on exception. This is particularly helpful when you need to check the state of your code after an error.

Debug on error

Debug on error

Auto Format Notebooks

Marimo has a built-in notebook format checker with the check command. You can fix format issues with the —fix flag.

Mariom lint

Mariom lint

Data Manipulations: SQL, and Visual No-Code Transformations

Establish connections to the DB and query them directly with SQL.

SQL connections

SQL connections

You can use it to start a no-code transformation on dataframes. You can also access the corresponding transformation code.

Use the UI to transform data without code.

No code transformations

No code transformations

Click on the </> Python Code tab to see its corresponding transformation code.

No code transformations - code gen

No code transformations - code gen

Reactivity & Dependency Tracking

When a variable changes, its dependent variables will change as well (aka reactivity). Marimo tracks the variable’s dependence across the entire notebook.

You can see the dependency graph for any variable using the dependency tab.

dependency graph

dependency graph

Note A variable can be initialized in only one cell, as it makes maintenance much simpler.

Conclusion

To recap, we saw how marimo enables you to write notebooks that are

  1. Version controllable
  2. Reusable
  3. Testable
  4. Easy to debug
  5. Auto formatable
  6. Easy to explore data: SQL, Dataframe, and no-code
  7. Reactive

Tired of using Jupyter and having to manually check the order of code execution, tired of having to copy and paste code between notebooks and IDEs?

Give marimo a try. You can quickly convert your jupyter notebooks to marimo with marimo convert your_notebook.ipynb -o your_notebook.py command and get started today!

Back to top