development_docs/adding_lint_rules.md
This guide explains how to add new lint rules to marimo's linting system.
marimo's lint system helps users write better, more reliable notebooks by detecting various issues that could prevent notebooks from running correctly. The system is organized around three severity levels:
Rule codes follow a specific pattern: M[severity][number]
When adding a new rule:
Example assignments:
Create your rule in the appropriate directory:
marimo/_lint/rules/breaking/marimo/_lint/rules/runtime/marimo/_lint/rules/formatting/Template for a new rule:
# Copyright 2026 Marimo. All rights reserved.
from __future__ import annotations
from typing import TYPE_CHECKING
from marimo._lint.diagnostic import Diagnostic, Severity
from marimo._lint.rules.base import LintRule
if TYPE_CHECKING:
from marimo._lint.context import RuleContext
class YourNewRule(LintRule):
"""MB005: Brief description of what this rule checks.
Detailed explanation of what this rule does and why it's important.
This should explain the technical details of how the rule works.
## What it does
Clear, concise explanation of what the rule detects.
## Why is this bad?
Explanation of why this issue is problematic:
- Impact on notebook execution
- Potential for bugs or confusion
- Effect on reproducibility
## Examples
**Problematic:**
```python
# Example of code that violates this rule
bad_code = "example"
```
**Solution:**
```python
# Example of how to fix the violation
good_code = "example"
```
## References
- [Understanding Errors](https://docs.marimo.io/guides/understanding_errors/)
- [Relevant Guide](https://docs.marimo.io/guides/...)
"""
code = "MB005" # Your assigned code
name = "your-rule-name" # Kebab-case name
description = "Brief description for rule listings"
severity = Severity.BREAKING # Or RUNTIME/FORMATTING
fixable = False # True if rule can auto-fix issues
async def check(self, ctx: RuleContext) -> None:
"""Implement your rule logic here."""
# Iterate through notebook cells
for cell in ctx.notebook.cells:
# Your detection logic here
if self._detect_violation(cell):
diagnostic = Diagnostic(
message="Description of the specific violation",
line=cell.lineno,
column=cell.col_offset + 1,
code=self.code,
name=self.name,
severity=self.severity,
fixable=self.fixable,
)
await ctx.add_diagnostic(diagnostic)
def _detect_violation(self, cell) -> bool:
"""Helper method for detection logic."""
# Implement your specific detection logic
return False
Add your rule to the appropriate __init__.py file:
For Breaking rules (marimo/_lint/rules/breaking/__init__.py):
from marimo._lint.rules.breaking.your_file import YourNewRule
BREAKING_RULE_CODES: dict[str, type[LintRule]] = {
"MB001": UnparsableRule,
"MB002": MultipleDefinitionsRule,
"MB003": CycleDependenciesRule,
"MB004": SetupCellDependenciesRule,
"MB005": YourNewRule, # Add your rule here
}
__all__ = [
# ... existing rules ...
"YourNewRule", # Add to exports
"BREAKING_RULE_CODES",
]
Create tests/_lint/test_files/your_rule_name.py:
import marimo
__generated_with = "0.15.2"
app = marimo.App()
@app.cell
def _():
# Code that should trigger your rule
problematic_code = "example"
return
@app.cell
def _():
# Additional test cases
return
if __name__ == "__main__":
app.run()
Add to tests/_lint/test_snapshot.py:
def test_your_rule_snapshot():
"""Test snapshot for your new rule."""
file = "tests/_lint/test_files/your_rule_name.py"
with open(file) as f:
code = f.read()
notebook = parse_notebook(code, filepath=file)
errors = lint_notebook(notebook)
# Format errors for snapshot
error_output = []
for error in errors:
error_output.append(error.format())
snapshot("your_rule_name_errors.txt", "\n".join(error_output))
For more rigorous testing, create tests/_lint/test_your_rule.py:
import pytest
from marimo._ast.parse import parse_notebook
from marimo._lint.context import LintContext
from marimo._lint.rules.breaking import YourNewRule
class TestYourNewRule:
"""Test cases for YourNewRule."""
async def test_detects_violation(self):
"""Test that the rule detects violations correctly."""
code = """import marimo
app = marimo.App()
@app.cell
def _():
# Code that should trigger the rule
return
"""
notebook = parse_notebook(code)
ctx = LintContext(notebook)
rule = YourNewRule()
await rule.check(ctx)
diagnostics = await ctx.get_diagnostics()
assert len(diagnostics) > 0
assert diagnostics[0].code == "MB005"
assert diagnostics[0].severity == Severity.BREAKING
async def test_no_false_positives(self):
"""Test that the rule doesn't trigger on valid code."""
code = """import marimo
app = marimo.App()
@app.cell
def _():
# Valid code that should not trigger the rule
return
"""
notebook = parse_notebook(code)
ctx = LintContext(notebook)
rule = YourNewRule()
await rule.check(ctx)
diagnostics = await ctx.get_diagnostics()
assert len(diagnostics) == 0
The documentation is automatically generated from your rule's docstring. Run:
uv run scripts/generate_lint_docs.py
This will create:
docs/guides/lint_rules/rules/your_rule_name.mddocs/guides/lint_rules/index.md# Run lint tests
uv run --group test pytest tests/_lint
# Run your specific test
uv run --group test pytest tests/_lint/test_your_rule.py
# Update snapshots if needed
uv run --group test pytest tests/_lint/test_snapshots.py --snapshot-update
RuleContext (notebook, graph, etc.)Good: "Variable 'x' is defined in multiple cells"
Bad: "Multiple definition error"
Safely fixable errors are applied by default via re-serialization. However,
rules may implement unsafe fixes (mutating the notebook structure) that require
the --unsafe-fixes flag. To do this, implement an async def apply_unsafe_fixes(self, notebook, diagnostics) -> Notebook method in your
rule class, and inherit from UnsafeFixRule instead of LintRule.
async def check(self, ctx: RuleContext) -> None:
for cell in ctx.notebook.cells:
if self._check_cell(cell):
# Create diagnostic
async def check(self, ctx: RuleContext) -> None:
graph = ctx.get_graph()
for cell_id, cell_data in graph.cells.items():
# Analyze dependencies
tests/_lint/
├── test_files/ # Test notebooks
│ └── your_rule_name.py
├── snapshots/ # Expected outputs
│ └── your_rule_name_errors.txt
├── test_your_rule.py # Unit tests
└── test_snapshots.py # Snapshot tests
Your rule's docstring should include:
The documentation system will automatically:
Before submitting your rule:
__init__.pyExample of a simple rule that checks for syntax errors: https://github.com/marimo-team/marimo/pull/6384
--unsafe-fixesSome rules may have "fixes" that mutate the notebook structure. An example of a rule that mutates the notebook structure (i.e. an unsafe fix) by removing empty cells: https://github.com/marimo-team/marimo/pull/6398
Some parsing issues result in log warnings or errors. An example of a rule that hooks into issued logs can be found here:
Note: Resist intentionally adding log statements such that they trigger lint rules. Log statements should be added only when they provide useful context, on notebook startup.