sqlnbfmt
Quick start:
pip install sqlnbfmt && sqlnbfmt notebook.ipynb
A SQL formatter for Jupyter Notebooks and Marimo notebooks. sqlnbfmt automatically formats SQL queries embedded in notebook cells — Python strings, SQL magic cells (%%sql), and Marimo mo.sql() calls — helping you maintain clean and consistent code.
Features
- Zero-config: Works out of the box with sensible defaults — no config file needed
- Jupyter + Marimo: Formats SQL in
.ipynbnotebooks and Marimo.pynotebooks - Smart SQL Detection: Automatically identifies and formats SQL queries in code cells, magic SQL cells, and
mo.sql()calls - AST-Powered: Uses Abstract Syntax Tree parsing for accurate SQL string identification
- Safe Formatting: Preserves Python comments, query parameters (e.g.,
%s,?), f-string placeholders, and SQL comments - CI-Friendly:
--checkmode exits non-zero when formatting is needed;--diffshows what would change - Skip Hints: Add
# sqlnbfmt: skipto any cell to exclude it from formatting - Pre-commit Ready: Seamlessly integrates with pre-commit hooks
- JupyterLab Plugin: Works with
jupyterlab-code-formatterfor interactive format-on-save - Lightweight: Only three runtime dependencies (sqlglot, nbformat, pyyaml)
Installation
pip install sqlnbfmt
Usage
Command Line
Format Jupyter notebooks:
sqlnbfmt path/to/your_notebook.ipynb
Format Marimo notebooks (or any Python file with SQL strings):
sqlnbfmt path/to/your_notebook.py
Mix both in a single invocation:
sqlnbfmt *.ipynb marimo_app.py
Check formatting without modifying files (useful in CI):
sqlnbfmt --check path/to/your_notebook.ipynb path/to/marimo_app.py
Show a diff of what would change:
sqlnbfmt --diff path/to/your_notebook.ipynb
Skipping Cells
Add a # sqlnbfmt: skip comment to skip formatting. In Jupyter notebooks this applies per-cell; in Python files it applies to the entire file.
# sqlnbfmt: skip
query = "select * from my_special_table where id = 1"
JupyterLab Integration
Format SQL cells interactively with jupyterlab-code-formatter:
pip install sqlnbfmt jupyterlab-code-formatter
That's it — sqlnbfmt auto-registers as a Jupyter server extension. In JupyterLab, use Edit > Format Cell or configure format-on-save in Settings > Code Formatter, and select "Apply SQL Notebook Formatter".
The extension uses [tool.sqlnbfmt] (including dialect) from the nearest pyproject.toml of the directory you start Jupyter in. To set the dialect explicitly instead, add this to ~/.jupyter/jupyter_server_config.py:
from sqlnbfmt.jupyterlab_integration import register
register(dialect="postgres")
Pre-commit Integration
- Install pre-commit:
pip install pre-commit
- Add to
.pre-commit-config.yaml:
repos:
- repo: https://github.com/flyersworder/sqlnbfmt
rev: v0.6.0
hooks:
- id: sqlnbfmt
types: [jupyter]
To also format Marimo notebooks (Python files), add the Python hook:
- id: sqlnbfmt-py
files: '\.py$' # optionally narrow with: files: 'marimo_.*\.py$'
All arguments are optional. Settings in [tool.sqlnbfmt] in your pyproject.toml are picked up automatically (see Configuration); alternatively pass them as arguments:
args: [--dialect, postgres, --config, config.yaml]
- Install the hook:
pre-commit install
- (Optional) Run on all files:
pre-commit run --all-files
CI Usage
Use --check in GitHub Actions to enforce formatting:
- name: Check SQL notebook formatting
run: |
pip install sqlnbfmt
sqlnbfmt --check **/*.ipynb
sqlnbfmt --check marimo_*.py # if using Marimo
Configuration
sqlnbfmt works without any configuration. To override defaults, use a [tool.sqlnbfmt] table in pyproject.toml or a separate YAML file.
pyproject.toml
[tool.sqlnbfmt]
dialect = "postgres"
extend_function_names = ["run_query"] # recognized in addition to the defaults
[tool.sqlnbfmt.formatting_options]
indent_width = 2
sqlnbfmt searches the current directory and its parents for the nearest pyproject.toml with a [tool.sqlnbfmt] table, stopping at the repository root (the directory containing .git). A pyproject.toml without that table is skipped, so a sub-package's file doesn't hide the repository-level configuration. The JupyterLab integration does the same, starting from the Jupyter server's working directory.
YAML file
Pass a YAML file with --config config.yaml. Here is a template. --config also accepts a .toml path, which is read from its [tool.sqlnbfmt] table.
When --config is given, pyproject.toml discovery is skipped. Options you don't set keep their defaults, and unknown option names produce a warning.
Configuration Options
| Option | Description | Default |
|---|---|---|
dialect |
SQL dialect passed to sqlglot (e.g. postgres, mysql, bigquery); --dialect overrides it |
none (generic SQL) |
sql_keywords |
SQL keywords to recognize and format | Common SQL keywords |
function_names |
Python functions containing SQL code (see matching) | read_sql, execute, sql (Marimo's mo.sql), etc. |
sql_decorators |
Decorators whose SQL is formatted: string arguments of @query("SELECT ..."), and strings returned by a function decorated with @query |
query, db_query, sql_query, sql_decorator |
extend_sql_keywords, extend_function_names, extend_sql_decorators |
Entries added to the corresponding list instead of replacing it | none |
formatting_options.single_line_threshold |
Deprecated: never had any effect and will be removed. Line breaks are decided by sqlglot | |
formatting_options.indent_width |
Number of spaces for indentation | 4 |
To add to the built-in lists, use the extend_* options. sql_keywords, function_names and sql_decorators replace the built-in lists, so setting function_names = ["run_query"] drops sql, and Marimo's mo.sql() calls stop being formatted. If you do replace a list, copy the defaults you still rely on from the template. You can set both: the extend_* entries are added to your replacement list.
How function and decorator names match
A call counts as SQL-bearing when a configured name appears anywhere in its full dotted name. Matching is case-sensitive. This is deliberately permissive, so that wrappers and variants are covered without configuration:
executematchescursor.execute,cursor.executemanyandexecute_sqlsqlmatchesmo.sql,spark.sql, pandasql'ssqldf/pysqldfandsqlalchemy.textsqldoes not matchrunSQLQuery, because matching is case-sensitive; addSQLif you need it
Decorator names in sql_decorators match the same way, against the decorator's dotted name (query matches @query, @db_query and @app.query(...)).
A matched call's string arguments are only reformatted if they look like SQL. Still, prefer specific names: run would also match rerun(...) and subprocess.run(...), so use run_query rather than run.
Examples
Jupyter Notebook
Before formatting:
execute_sql("""SELECT a.col1, b.col2 FROM table_a a JOIN table_b b ON a.id = b.a_id WHERE a.status = 'active' ORDER BY a.created_at DESC""")
After formatting:
execute_sql(
"""
SELECT
a.col1,
b.col2
FROM table_a AS a
JOIN table_b AS b
ON a.id = b.a_id
WHERE
a.status = 'active'
ORDER BY
a.created_at DESC
"""
)
Marimo Notebook
Before formatting:
@app.cell
def _(mo):
_df = mo.sql(f"select id, name from users where active = 1 order by name")
return
After formatting:
@app.cell
def _(mo):
_df = mo.sql(
f"""
SELECT
id,
name
FROM users
WHERE
active = 1
ORDER BY
name
"""
)
return
Troubleshooting
SQL not being formatted?
- Ensure the string contains at least 2 SQL keywords or a recognizable pattern like
SELECT...FROM - Check that the function name is in the recognized list (add custom ones via
extend_function_namesin[tool.sqlnbfmt]or your--configfile)
Comments being modified?
- Python comments (
#) are preserved. SQL comments (--) inside strings are converted to/* */block comments by sqlglot.
Pre-commit hook fails?
- Make sure the
revmatches the installed version - Run
pre-commit autoupdateto get the latest version
Contributing
We welcome contributions! Here's how to get started:
- Clone the repository:
git clone https://github.com/flyersworder/sqlnbfmt.git
cd sqlnbfmt
- Use
uvto sync the environment:
uv sync
source .venv/bin/activate # On Windows: .venv\Scripts\activate
- Run tests:
pytest
Before a release, also run the live end-to-end tests. They build the wheel, install it into a fresh virtualenv with Jupyter Server and jupyterlab-code-formatter, and exercise the CLI, the pre-commit hooks and the Jupyter extension against a scratch project. They need uv, git and network access, so the default pytest run skips them:
uv run pytest -m live
-
Add eval cases: see
tests/eval/generate_fixtures.pyfor examples. Runpython tests/eval/generate_fixtures.pyto regenerate fixtures. -
Install dev pre-commit hooks:
pre-commit install
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- sqlglot - SQL parsing and formatting engine
- All contributors and early adopters who helped shape this tool
Made with ♥️ by the sqlnbfmt team
Metadata
Release files for sqlnbfmt 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| sqlnbfmt-0.6.0.tar.gz | 93.7 kB | With provenance | Details for sqlnbfmt-0.6.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| sqlnbfmt-0.6.0-py3-none-any.whl | Python 3 | none | any | Details for sqlnbfmt-0.6.0-py3-none-any.whl |
Total release size: 118.3 kB
Release files / sqlnbfmt-0.6.0.tar.gz
| Download URL | sqlnbfmt-0.6.0.tar.gz |
|---|---|
| Size | 93.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1d994b33d0bf1b246da9f565c947555ee752971f102c4162ccf1bb56e2db5b84
|
|
BLAKE2b-256 checksum How to use checksums |
293b6523722afdb17b7d939f69f4e513ea2b136bbaa8585e4a9511f4465c5c2a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / sqlnbfmt-0.6.0-py3-none-any.whl
| Download URL | sqlnbfmt-0.6.0-py3-none-any.whl |
|---|---|
| Size | 24.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
66a325a04a3e011dd1c6d9e6bbf35b313c4e4e29c9fbbf147a264d357af21b51
|
|
BLAKE2b-256 checksum How to use checksums |
c89baa981052e70bba87607d258a068318c92d56d9f61902b5f80cd91e5a434c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logSecurity for sqlnbfmt 0.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for sqlnbfmt-0.6.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.