
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
