mrkeyoor.com_
Thu 06 Aug 02:40 UTC
PyPIDataupdated 06 Aug 2026

sqlparse

sqlparse turns a blob of SQL text into a tree of tokens you can walk, reformat, or split apart. Three module-level functions cover nearly every use: split() cuts a script into individual statements while respecting strings, comments, and semicolons inside blocks; format() pretty-prints with options like reindent, keyword_case, and strip_comments; and parse() returns Statement objects whose tokens are either leaves with a ttype or groups such as Identifier, IdentifierList, Where, Function, and Parenthesis that you can descend into. The critical word in the project description is non-validating. It accepts any input, never raises on bad SQL, and assumes no particular dialect, which is why templated and vendor-specific SQL parses at all. It is pure Python with zero dependencies and also installs a sqlformat command line tool.

Verdict

The right size of tool for splitting scripts, pretty-printing SQL, and shallow introspection, which is why it sits under Django and half the Python data tooling. Do not mistake the token tree for an AST, and keep it away from untrusted input without a size cap and a timeout.

API stability5/5split(), format(), and parse() plus the sqlparse.sql class hierarchy have been stable across the 0.4 and 0.5 lines. The 0.5.5 change that raises SQLParseError instead of silently returning None when grouping limits are exceeded is the only recent behaviour change likely to reach your code.
Docs4/5sqlparse.readthedocs.io documents every format() keyword argument and autodocs the whole sql class hierarchy, and the rewritten README is a good short introduction with a real token tree printed out. What is missing is guidance on the traversal patterns everyone actually writes, so extracting table names is still folklore copied off Stack Overflow.
Maintenance3/5Pushed July 2026 with 0.5.5 released in December 2025, and the development branch carries real security fixes for quadratic grouping behaviour. Against that: 241 open issues (272 counting PRs) and a README that states the project is maintained in spare time. It is alive and it is slow.
Ecosystem5/5Around 34.7M weekly downloads, largely because Django depends on it, and it is the default answer for splitting or formatting SQL in Python. It carries no dependencies of its own, ships a sqlformat CLI and a pre-commit hook, and powers sqlformat.org.

Use it if

  • You need to split a .sql script into executable statements and a naive split on semicolons would break on semicolons inside string literals, comments, or BEGIN blocks
  • You want readable SQL in a log line, an admin page, or an error message: format(sql, reindent=True, keyword_case='upper') is a one-liner and this is exactly what the Django debug toolbar does
  • You need light introspection of arbitrary SQL: what type of statement is this, which identifiers and aliases appear, what is in the WHERE clause, without committing to a dialect or a grammar
  • You want a dependency-free pure Python package that installs anywhere, including locked-down environments where a Rust or C extension is not an option
  • You want a pre-commit hook that reformats .sql files in place, which the repository ships as the sqlformat hook
Skip it if

Setup reality

pip install sqlparse is about as painless as Python packaging gets: pure Python, no dependencies, wheels for everything, and Python 3.8 or newer for the current 0.5.5 release. The development branch raises that floor to 3.10, so pin if you are on an older interpreter. Two things surprise people after install. First, the package name collides with expectations from ORMs: Django already depends on sqlparse for sqlmigrate and debug output, so it is probably in your environment whether you asked for it or not, and pinning a version can conflict with Django's own range. Second, the mental model. parse() returns a tuple of statements, not one statement, so the [0] you see everywhere is not optional. Tokens come in two kinds: leaves carry a ttype from sqlparse.tokens and groups carry None and hold children, and almost every walk you write needs to handle both plus whitespace tokens that appear between everything. Whitespace is preserved, so any comparison against str(token) has to account for it or use token.normalized. Installing also puts a sqlformat script on PATH, which reads stdin when the filename is '-' and can rewrite files with --in-place.

Patterns

Cut a multi-statement script into statementssplit-a-script

import sqlparse

sqlparse.split("select * from foo; select * from bar;")
# ['select * from foo;', 'select * from bar;']

sqlparse.split("select 1; select 2;", strip_semicolon=True)
# ['select 1', 'select 2']

This is the main reason to install the library: it knows a semicolon inside a string literal or a comment is not a separator. Pass strip_semicolon=True when you are handing each statement to a driver that rejects trailing semicolons.

Pretty-print a queryformat-sql

print(sqlparse.format(
    "select id,name from users where active=1 and x in (1,2)",
    reindent=True,
    keyword_case="upper",
    indent_width=4,
))
# SELECT id,
#        name
# FROM users
# WHERE active=1
#     AND x IN (1,
#               2)

reindent implies strip_whitespace, so the input's own line breaks are discarded. Useful options beyond these: identifier_case, strip_comments, comma_first, wrap_after, reindent_aligned, and compact. An invalid option value raises SQLParseError, which is the only place format() throws.

Inspect what a statement is made ofwalk-the-token-tree

statement = sqlparse.parse("select id, name from users where active = 1")[0]

print(statement.get_type())  # 'SELECT'

for token in statement.tokens:
    if token.is_whitespace:
        continue
    print(f"{token.ttype or type(token).__name__!s:20} {token}")
# Token.Keyword.DML    select
# IdentifierList       id, name
# Token.Keyword        from
# Identifier           users
# Where                where active = 1

parse() returns a tuple, hence the [0]. A token with a ttype is a leaf; a token whose ttype is None is a group you can recurse into via token.tokens. Whitespace is preserved as real tokens, so skip it explicitly in every loop.

Pull names, aliases, and qualifiers out of a select listread-identifiers-and-aliases

from sqlparse.sql import IdentifierList

sql = "select a.id, b.name as bn from users a join orgs b on a.oid = b.id"
statement = sqlparse.parse(sql)[0]

for token in statement.tokens:
    if isinstance(token, IdentifierList):
        for ident in token.get_identifiers():
            print(ident.get_real_name(), ident.get_alias(), ident.get_parent_name())
# id   None  a
# name bn    b

get_name() returns the alias when there is one and the real name otherwise, which is usually what you want for output columns. A single-column select produces a bare Identifier rather than an IdentifierList, so handle both types or you will silently skip those queries.

Collect the tables a query reads fromextract-table-names

from sqlparse.sql import Identifier, IdentifierList
from sqlparse.tokens import Keyword

SOURCES = {"FROM", "JOIN", "INNER JOIN", "LEFT JOIN", "LEFT OUTER JOIN"}

def table_names(statement):
    names, expecting = [], False
    for token in statement.tokens:
        if token.is_whitespace:
            continue
        if expecting:
            if isinstance(token, IdentifierList):
                names += [i.get_real_name() for i in token.get_identifiers()]
            elif isinstance(token, Identifier):
                names.append(token.get_real_name())
            expecting = False
        elif token.ttype is Keyword and token.normalized in SOURCES:
            expecting = True
    return names

table_names(sqlparse.parse("select * from public.users u join orgs o on 1=1")[0])
# ['users', 'orgs']

This walks the top level only, so subqueries, CTEs, and derived tables are missed unless you recurse into group tokens. There is no built-in table extractor and every version of this snippet on the internet has holes; if you need it to be right, use sql-metadata or sqlglot.

Read comparisons out of a WHERE clauseinspect-the-where-clause

from sqlparse.sql import Where, Comparison

statement = sqlparse.parse("select * from t where a = 1 and b > 2")[0]
where = next(t for t in statement.tokens if isinstance(t, Where))

for token in where.tokens:
    if isinstance(token, Comparison):
        print(token.left, token.right)
# a 1
# b 2

Comparison exposes .left and .right but not the operator as an attribute; read it from the tokens between them. OR branches, NOT, and parenthesised groups are all just more nested tokens, so anything past a flat AND list needs recursion.

Iterate every leaf token, ignoring the treeflatten-tokens

from sqlparse.tokens import Keyword, DML

statement = sqlparse.parse("select a.id from t join u on 1=1")[0]

keywords = [str(t) for t in statement.flatten() if t.ttype in (Keyword, DML)]
# ['select', 'from', 'join', 'on']

flatten() is the right tool for token-type counting and simple redaction. It is the wrong tool for names, because a qualified identifier like public.users flattens into three tokens: 'public', '.', 'users'.

Remove comments before logging or hashing a querystrip-comments

sqlparse.format(
    "select 1 -- inline note\n, 2 /* block */ from t",
    strip_comments=True,
    strip_whitespace=True,
)
# 'select 1 , 2 from t'

Handy for grouping query shapes in a slow-query log, since ORMs and BI tools inject per-request comments that otherwise make every query unique. It removes the comment text but not the space it occupied, so normalise whitespace too.

Shorten long string literals before storing a querytruncate-literals

sqlparse.format(
    "insert into t values ('averylongstring')",
    truncate_strings=8,
)
# "insert into t values ('averylon[...]')"

This is the closest thing to redaction the library offers, and it is not one: it shortens literals, it does not identify sensitive ones, and numbers are untouched. Set truncate_char to change the '[...]' marker.

Remember that nothing raises for bad SQLhandle-invalid-input

statement = sqlparse.parse("this is not sql at all ((")[0]
print(statement.get_type())  # 'UNKNOWN'

print(repr(sqlparse.format("select from where", reindent=True)))
# 'select\nfrom\nwhere'

get_type() returning 'UNKNOWN' is the only signal you get, and plenty of genuinely broken SQL still returns 'SELECT'. If your pipeline needs validation, run the statement through the target database's parser or use sqlglot, which raises on syntax it cannot parse.

Bound the work when input is not yoursguard-against-dos

from sqlparse.exceptions import SQLParseError

MAX_BYTES = 64 * 1024

def safe_parse(sql):
    if len(sql) > MAX_BYTES:
        raise ValueError("query too large to parse")
    try:
        return sqlparse.parse(sql)
    except SQLParseError as exc:
        # e.g. 'Maximum grouping depth exceeded (100).'
        raise ValueError(f"unparseable: {exc}") from exc

The depth cap is a backstop against deeply nested parentheses, not a time limit. Wide statements and the quadratic grouping paths fixed on the development branch are not covered by it, so keep the size bound and run parsing off the request thread if the source is public.

Format files from the shell or a pre-commit hookuse-the-cli

sqlformat --reindent --keywords upper query.sql
cat query.sql | sqlformat --reindent -

# .pre-commit-config.yaml
# repos:
#   - repo: https://github.com/andialbrecht/sqlparse
#     rev: 0.5.5
#     hooks:
#       - id: sqlformat
#         args: [--in-place, --reindent, --keywords, upper]

The hook defaults to --in-place --reindent. If you override args, keep --in-place or the hook writes to stdout and leaves your files exactly as they were, which looks like a passing hook that did nothing.

Alternatives

PackageRegistryPick it when
sqlglotPyPIYou need a real AST, dialect-aware parsing, query rewriting, or transpilation between engines.
sqlfluffPyPIYou want a configurable SQL linter and formatter to run in CI with rules and autofix.
sql-metadataPyPIYou only want table names, column names, and aliases out of a query and do not want to write the token walk yourself.