sqla-lint
A linter for SQLAlchemy queries that catches common anti-patterns — both at runtime (against live query objects) and statically (by analysing .py source files without executing them).
Installation
pip install sqla-lint
SQLAlchemy is an optional dependency. Install it alongside the linter to use the runtime API:
pip install "sqla-lint[sqlalchemy]"
CLI
The sqla-lint command performs static analysis on Python source files using AST parsing — no code is executed.
Basic usage
# Lint a single file
sqla-lint path/to/queries.py
# Recurse into a directory
sqla-lint src/
# Use a glob pattern (quote it to prevent shell expansion)
sqla-lint "src/**/*.py"
Options
| Flag | Description |
|---|---|
--skip RULE[,RULE…] |
Skip one or more rule IDs. Repeatable. |
--select RULE[,RULE…] |
Only report these rule IDs. Takes precedence over --skip. |
--min-severity LEVEL |
Minimum severity to report: error, warning, or info (default: info). |
--format text|json |
Output format (default: text). |
-q / --quiet |
Suppress all output; rely on exit code only. |
Examples
# Only report errors and warnings
sqla-lint src/ --min-severity warning
# Skip the "no LIMIT" and "count(*)" rules
sqla-lint src/ --skip SQLA004,SQLA006
# Only check for unbounded DELETEs and UPDATEs
sqla-lint src/ --select SQLA002,SQLA003
# Machine-readable JSON output (e.g. for CI pipelines)
sqla-lint src/ --format json
Exit codes
| Code | Meaning |
|---|---|
0 |
No violations found at or above --min-severity. |
1 |
One or more violations found. |
2 |
Usage / I/O error (no files found, unreadable path, etc.). |
Text output format
myapp/queries.py:14:4: [WARNING] SQLA001: Query uses SELECT * — explicitly list the columns you need
myapp/queries.py:27:8: [ERROR] SQLA002: DELETE statement has no WHERE clause — this will delete all rows in the table
sqla-lint: 2 violations: 1 error, 1 warning
Python API
Runtime linting
Pass a SQLAlchemy query object directly to sqla_lint.lint():
from sqlalchemy import select, text
import sqla_lint
stmt = select(text("*")).select_from(users)
result = sqla_lint.lint(stmt)
for v in result.violations:
print(f"[{v.severity.value.upper()}] {v.rule_id}: {v.message}")
LintResult provides filtered views:
result.has_violations # bool
result.errors # list[LintViolation] — Severity.ERROR only
result.warnings # list[LintViolation] — Severity.WARNING only
result.infos # list[LintViolation] — Severity.INFO only
Static file analysis
Analyse source files without importing or executing them:
from sqla_lint import parse_file, parse_source
# From a file path
for violation in parse_file("myapp/queries.py"):
print(violation) # myapp/queries.py:14:4: [WARNING] SQLA001: ...
# From a source string
violations = parse_source(source_code, filename="<inline>")
FileViolation has rule_id, message, severity, filename, line, and col attributes.
Configuration
Skipping rules
import sqla_lint
config = sqla_lint.LintConfig(skip_rules={"SQLA004", "SQLA006"})
result = sqla_lint.lint(stmt, config=config)
Overriding severity
from sqla_lint import LintConfig, RuleConfig, Severity
config = LintConfig(
rule_configs={
"SQLA004": RuleConfig(severity_override=Severity.ERROR),
}
)
result = sqla_lint.lint(stmt, config=config)
Disabling a rule via RuleConfig
config = LintConfig(
rule_configs={
"SQLA006": RuleConfig(enabled=False),
}
)
Using the Linter class directly
For repeated use (e.g. in tests or middleware), create a Linter instance once:
from sqla_lint import Linter, LintConfig
linter = Linter(config=LintConfig(skip_rules={"SQLA004"}))
result1 = linter.lint(stmt1)
result2 = linter.lint(stmt2)
Rules
| ID | Severity | Description |
|---|---|---|
| SQLA001 | warning | SELECT * — use explicit column names |
| SQLA002 | error | DELETE without WHERE clause — deletes every row |
| SQLA003 | error | UPDATE without WHERE clause — updates every row |
| SQLA004 | warning | SELECT without LIMIT — may return an unbounded result set |
| SQLA005 | warning | LIMIT without ORDER BY — row order is non-deterministic |
| SQLA006 | info | count(*) — prefer count(1) or count(primary_key) |
| SQLA007 | error | Multiple FROM tables without JOIN — Cartesian product (runtime only) |
| SQLA008 | warning | Query inside a loop — potential N+1 (static analysis only) |
SQLA007 requires type information and is only detected by the runtime linter (
sqla_lint.lint()), not by the CLI static analyser.SQLA008 is only detected by the static analyser (CLI /
parse_file) and is not checked at runtime.
Custom rules
Subclass Rule, set the three class attributes, implement check(), and pass an instance to Linter:
from typing import Any
from sqla_lint import Linter, LintConfig, Rule, LintViolation, Severity
class NoOffsetWithoutLimitRule(Rule):
rule_id = "CUSTOM001"
description = "OFFSET without LIMIT is almost always a bug"
default_severity = Severity.ERROR
def check(self, query: Any) -> list[LintViolation]:
offset = getattr(query, "_offset_clause", None)
limit = getattr(query, "_limit_clause", None)
if offset is not None and limit is None:
return [self._violation("OFFSET used without LIMIT")]
return []
linter = Linter(rules=[NoOffsetWithoutLimitRule()])
result = linter.lint(stmt)
To combine custom rules with the built-in ones:
from sqla_lint import ALL_RULES
linter = Linter(rules=list(ALL_RULES) + [NoOffsetWithoutLimitRule()])
Release files for sqla-lint 0.1.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 | |
|---|---|---|---|
| sqla_lint-0.1.0.tar.gz | 16.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sqla_lint-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:34.1 kB
Release files / sqla_lint-0.1.0.tar.gz
| Download URL | sqla_lint-0.1.0.tar.gz |
|---|---|
| Size | 16.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
46d7fb3ffe625bfff839afed4f5b2f49fbb9fa813f2ec2918ac4cb956edb8c3b
|
|
BLAKE2b-256 checksum How to use checksums |
08c25af236d3459b246087e2d919d7b66143dc47d4eadd238cb8a1843a7261f1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.5.11
|
Release files / sqla_lint-0.1.0-py3-none-any.whl
| Download URL | sqla_lint-0.1.0-py3-none-any.whl |
|---|---|
| Size | 17.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cc1c0122a5e1581d74a2b6cdc683e2e07fe9d5b05836871759d5a4f6408991be
|
|
BLAKE2b-256 checksum How to use checksums |
3ba9f9c526e2fa6b18650c6b9b193c4c9e050b9c8fff93040f981f6c1549743b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.5.11
|