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()])
Metadata
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 | Provenance | Actions |
|---|---|---|---|---|
| sqla_lint-0.1.0.tar.gz | 16.6 kB | Without provenance | Details for sqla_lint-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| sqla_lint-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for sqla_lint-0.1.0-py3-none-any.whl |
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
|
Security for sqla-lint 0.1.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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
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.