🍃 Announcing molab:

Read our announcement

marimo check: a notebook linter for agents and humans

marimo check: a notebook linter for agents and humans

In this blog post, we introduce marimo check, a linter that enables agents and humans alike to write high-quality notebooks, pipelines, and apps with marimo. We discuss the motivations behind its creation, the principles guiding its design, and how it integrates seamlessly into modern software development workflows, including CI pipelines and AI-assisted coding.

When writing traditional software, developers use linters to find bugs, errors, and code smells; because marimo notebooks are actually just Python programs, we believe they deserve similar tooling.

As one of our users put it:

“I think the right approach (to notebooks) is using things like IDEs, Git, uv, ruff, pre-commit etc. which in my opinion greatly helps any type of professional and should be a non-optional standard.”

This insight captures what we’re seeing across disciplines—professionals recognizing that software engineering practices are essential for anyone writing code that matters. Engineers in machine learning, data engineering, and AI, are already bought in to the idea of using linters, and want to bring that experience to marimo.

The demand became so strong that developers started building their own solutions. One community member even created a custom Pylint plugin specifically for marimo notebooks, checking for unused parameters in cell functions—clear evidence that the community was ready for better tooling.

Program validation tools like linters aren’t just for humans. In the age of coding agents, linters provide critical feedback to Claude Code, Gemini, and other agents, allowing them to iterate and self-correct without human intervention. While we’ve long understood that our community was interested in a marimo linter, it was this line of reasoning that motivated us to prioritize its development — especially given that more and more organizations are using Claude Code in conjunction with marimo to rapidly create new data workflows.

Today, we’re excited to introduce marimo check, our comprehensive answer to this community call.

A duality between marimo notebooks and Python programs

Software engineering has robust practices: unit testing, CI/CD, and comprehensive linting. In legacy notebooks like Jupyter, artifacts like figures and reports are sometimes considered as mattering more than the code itself. Since legacy cases have a whole host of problems in reproducibility, maintainability, and reusability; they are really nothing more than scratchpads and are not reusable software artifacts. Conversely, marimo is engineered to be reactive, maintainable, and even testable: creating an auditable record of your work and analysis.

marimo feels like a notebook but is actually stored as a pure Python program with reproducible execution semantics that you can push to production as apps or pipelines, or reuse as modules. marimo nudges you to write better code by default, because it constrains your code to be a dataflow graph.

Even with this foundation, we recognized the need for active guidance and tooling that catches potential issues before they become problems. While you previously needed to open a notebook to check whether constraints were satisfied, marimo check lets you (and agents) validate marimo notebooks for correctness from the command-line.

marimo check in action

Let’s see marimo check in action with a realistic data engineering example:

import marimo
 
app = marimo.App()
 
@app.cell
def load_data():
    import pandas as pd
    sales_data = pd.read_csv("q3_sales.csv")
    processed_count = len(sales_data)  # Track record count
    return (sales_data,)
 
@app.cell
def transform_data(sales_data):
    processed_count = sales_data.shape[0] * 2  # Pipeline metric
    revenue_adjusted = sales_data['revenue'] * 1.1
    return (revenue_adjusted,)

Running marimo check on this notebook reveals several issues:

critical[multiple-definitions]: Variable ‘processed_count’ is defined in multiple cells
 —> data_engineering.py:10:1
  10 |     sales_data = pd.read_csv(“q3_sales.csv”)
  11 |     processed_count = len(sales_data)  # Track record count
     |     ^
  12 |     return (sales_data,)
…
  16 | def _(sales_data):
  17 |     processed_count = sales_data.shape[0] * 2  # Pipeline metric
     |     ^                                  18 |     revenue_adjusted = sales_data[‘revenue’] * 1.1
hint: Variables must be unique across cells. Alternatively, they can be private with an underscore prefix (i.e.`_processed_count`.)
Found 1 issue.

Notice how the error messages are actionable—they don’t just tell you what’s wrong, they suggest specific fixes.

Learning from the best error messages

When designing marimo check, we studied two exemplars of error communication: Ruff and Rust.

Ruff has revolutionized Python linting not just through speed, but through its crystal-clear, actionable error messages. Every diagnostic tells you not just what’s wrong, but how to fix it.

Rust takes this philosophy even further—its compiler errors are legendary for guiding developers toward solutions rather than just pointing out problems. However, Rust’s exemplar error messages didn’t evolve overnight; they evolved through years of community feedback and iteration (see The Evolution of Rustc errors).

Based on our study of these tools, we’ve made some opinionated choices in our error messaging style. Importantly, marimo check won’t duplicate detailed type checking or general code health issues, since tools like Ruff already excel at that. Instead, we concentrate on marimo-specific rules that ensure your notebooks can run reliably, reproducibly, and execute cleanly.

Notebooks compatible with your workflow

In the initial early discussion regarding code quality, @patrick-kidger noted:

“I don’t think marimo should seek to reinvent every orthogonal piece of tooling — this has always been one of the greatest weaknesses of Jupyter notebooks.”

We completely agree with this principle. marimo check is designed specifically to complement existing tools like ruff, pylint, and mypy: not to replace them. Our focus is exclusively on marimo-specific rules that ensure your notebooks are robust and executable.

We’ve been methodically laying groundwork for better tool integration, guided by a core principle

“If there is synergy between a Python best-practice and a marimo best-practice, then prefer changing marimo to better align with the Python practice.”

This philosophy shows up in concrete decisions. Take our choice to name default cells with _—this simple convention makes marimo notebooks compatible with tools like pyright, which treat underscore-prefixed variables as intentionally unused. We’ve also implemented automatic pruning of unused definitions and improved notebook serialization consistency.

These aren’t flashy features, but they represent careful engineering to ensure marimo notebooks integrate seamlessly with the broader Python ecosystem.

The linter extends this philosophy with rules that catch fundamental issues like formatting problems but also the core issues that can break notebook execution or reproducibility like multiple variable definitions, and circular dependencies.

Automated fixes: --fix and --unsafe-fixes

marimo check includes a --fix flag that automatically resolves simple issues:

marimo check --fix .

For more complex scenarios where the fix might change code behavior, we provide --unsafe-fixes (thanks, Ruff!) that makes the linter’s best guess at cleaning up your notebook.

We want to empower users to quickly find issues and iterate on them, not get bogged down in manual fixes for problems that have obvious solutions.

CI integration: quality gates for notebooks

marimo check easily folds into your CI pipeline to ensure a baseline quality for all notebooks:

# .github/workflows/check-notebooks.yml
name: Check Notebooks
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: astral-sh/setup-uv@v1
      - run: uv run marimo check --strict .

The --strict flag ensures that warnings are treated as errors in CI, maintaining high code quality standards across your team.

In fact, we’ve already integrated this into our own CI pipeline.

The age of agentic development

Here’s where things get really exciting: users now include more than just humans. AI agents like Claude Code are becoming integral to development workflows, and that’s especially true for marimo.

marimo team member Vincent has written a fantastic series on incorporating AI into your workflow, and we’re seeing LLMs excel at producing marimo notebooks. After all, they’re just Python!

But LLMs sometimes struggle with marimo-specific rules, such as the rule on variable redefinition. marimo check gives them a mechanism to get feedback on these issues and correct them automatically.

Our error messages are designed for both humans and agents. The --format=json output mode pairs perfectly with tools like jq:

marimo check --format=json notebook.py | jq '.issues[] | select(.severity == "breaking")'

A hot tip: create sub-agents for linting. This lets Claude iterate until the notebook is clean without eating into your primary context. Here’s a simple pattern:

Instructions for the marimo-lint agent (include in ~/.claude/agents/marimo-lint.md)

---
name: marimo-lint
description: Specialized edit agent that fixes lint issues in marimo notebooks before reporting back changes. Takes proposed edits, runs `marimo check`, fixes any issues, and reports the success of changes.
tools: Read, Edit, MultiEdit, Bash, Grep, Glob
model: sonnet
color: purple
---
 
You are a specialized edit agent focused on making lint-compliant code changes. Your workflow is:
 
1. **Receive Instructions**: You will be given either:
   - Specific edit instructions (file path, old content, new content)
   - OR files to process in tests/_lint/test_files/ directory for general cleanup
 
2. **Apply Initial Changes**:
   - If given specific edits: Make the requested edits using Edit or MultiEdit tools
   - If given files to clean: Analyze each file for lint issues and fix them systematically
 
3. **Run Lint Check**: Execute `uv run marimo check --fix <filepath>` to check for any lint issues in the modified files.
   - You may use `... marimo --format=json ...` if you need an output usable with jq.
 
4. **Fix Lint Issues**: If lint issues are found:
   - Analyze the lint output to understand the issues
   - Make additional edits to fix the lint problems
   - Repeat lint checking until no issues remain
   - Focus on issues reported in the docs: https://marimo.dev/docs/linting
   - Do not change the original intent of the code, only fix lint issues.
 
5. **Report Final Changes**: Return a summary of:
   - The final changes that were applied
   - Any additional lint fixes that were made
   - Final status of the files (lint clean)
   - If any lint issues couldn't be automatically fixed, report them
 
**Important Guidelines**:
- Always preserve the intent of the original edit request
- Make minimal additional changes needed for lint compliance
- Never change functionality, only style/lint issues
- If lint issues can't be fixed/easily automatically, report them clearly
- Always verify lint passes before reporting success. Only report success if `uv run marimo check` exits with status code 0.
- Be concise in your final report - focus on what was actually changed
 
**Response Format**:
Your final message should follow one of the formats below:
 
## Lint Status

This workflow integrates with our new ACP (AI Agents) feature and lets agents write, check, and refine marimo notebooks in a seamless loop.

Open to contributions

We’ve designed our development documentation to make contributing new lint rules as straightforward as possible. Check out our developer guide if you have ideas for marimo-specific code health rules.

We’re also eager to collaborate with other “checkers” interested in incorporating our rules. If you’re building tools that work with marimo notebooks, get in touch- we’ll work together to make things happen.

Community and growth

We’re expanding the rule set and improving the developer experience. Our comprehensive documentation covers all current rules, with examples and clear explanations of why each rule matters.

But we need your feedback. What marimo-specific issues do you encounter? What rules would help you write better notebooks? What integration points would make your workflow smoother? Please join the discussion on GitHub or Discord.

Happy checking! 🔍


Try marimo check today:

uvx marimo check your_notebook.py

Explore our complete documentation at docs.marimo.io