Skip to main content

sqlnbfmt

PyPI Tests License Python Versions

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 .ipynb notebooks and Marimo .py notebooks
  • 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: --check mode exits non-zero when formatting is needed; --diff shows what would change
  • Skip Hints: Add # sqlnbfmt: skip to any cell to exclude it from formatting
  • Pre-commit Ready: Seamlessly integrates with pre-commit hooks
  • JupyterLab Plugin: Works with jupyterlab-code-formatter for 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

  1. Install pre-commit:
pip install pre-commit
  1. 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]
  1. Install the hook:
pre-commit install
  1. (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:

  • execute matches cursor.execute, cursor.executemany and execute_sql
  • sql matches mo.sql, spark.sql, pandasql's sqldf/pysqldf and sqlalchemy.text
  • sql does not match runSQLQuery, because matching is case-sensitive; add SQL if 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_names in [tool.sqlnbfmt] or your --config file)

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 rev matches the installed version
  • Run pre-commit autoupdate to get the latest version

Contributing

We welcome contributions! Here's how to get started:

  1. Clone the repository:
git clone https://github.com/flyersworder/sqlnbfmt.git
cd sqlnbfmt
  1. Use uv to sync the environment:
uv sync
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
  1. 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
  1. Add eval cases: see tests/eval/generate_fixtures.py for examples. Run python tests/eval/generate_fixtures.py to regenerate fixtures.

  2. 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)

Source distribution for sqlnbfmt 0.6.0
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)

Table of built distributions (wheels) for sqlnbfmt 0.6.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page