Bindproof
Bindproof Bindproof is a small, offline linter that catches high-confidence Python DB-API binding mistakes before the affected code path reaches a database. It looks at literal SQL passed to .execute() and .executemany() and compares the placeholders with statically visible parameter containers. It catches the recurring mistakes that are easy to overlook in review: two ? or %s markers paired with a one-item tuple; a scalar (user_id) where a singleton tuple (user_id,) was intended; named markers whose dictionary is missing a required key; positional markers paired with a dictionary (or named markers with a list); one malformed row hidden in a literal executemany() batch; placeholders with no parameter argument; and mixed or incorrectly selected PEP 249 placeholder styles. Bindproof reads source with Python's AST. It never imports the files it checks, connects to a database, uses credentials, sends telemetry, or makes network requests. Install Bindproof has no runtime dependencies and supports modern Python 3 versions (3.10 or newer). Copy main.py and bindproof.py into a project or a tools directory. No package installation is required. Run its tests with pytest if you are modifying the tool: python -m pytest Usage Check the current directory recursively: python main.py Check individual files or directories: python main.py src tests/helpers.py Bindproof recognizes every PEP 249 paramstyle: qmark (?), numeric (:1), named (:name), format (%s), and pyformat (%(name)s). By default it infers the style independently for each literal SQL string. Select the adapter's style when you want the linter to enforce it: python main.py src --paramstyle pyformat Marker-like text inside SQL strings, quoted identifiers, and line/block comments is ignored. So are escaped %%, PostgreSQL :: casts, and the ?| and ?& operators. Worked example Given users.py: cursor.execute( "UPDATE users SET email = ? WHERE id = ?", (email,), ) cursor.execute( "SELECT * FROM users WHERE organization_id = :org AND active = :active", {"org": organization_id}, ) Run: python main.py users.py Bindproof reports editor-friendly locations and concrete corrections: users.py:1:1: BND004 expected 2 positional value(s), found 1; make the container length match the placeholders users.py:6:1: BND005 mapping is missing required key(s) 'active'; add them The corrected bindings are: cursor.execute( "UPDATE users SET email = ? WHERE id = ?", (email, user_id), ) cursor.execute( "SELECT * FROM users WHERE organization_id = :org AND active = :active", {"org": organization_id, "active": True}, ) CI behavior The process exit status is designed for pre-commit hooks and CI: 0: checked files are clean; 1: one or more binding mismatches were found; 2: a path could not be read or a Python file has invalid syntax. For example: python main.py src tests Diagnostics are written to standard output. Path, read, and syntax errors are written to standard error. Directory traversal is deterministic, overlapping path arguments are deduplicated, and common VCS, cache, virtual-environment, dependency, and build directories are skipped. Deliberate limits Bindproof is a conservative binding-shape linter, not a database or a full SQL parser. It deliberately does not: guess about dynamically built SQL or dynamically produced parameters; perform deep interprocedural data-flow or runtime type inference; model driver-specific placeholders, extensions, or coercions; validate SQL syntax, table/column names, transactions, or query results; judge the types or values inside parameter containers; detect SQL injection or provide a general security audit; or guarantee that every possible runtime binding error will be found. When a container uses *items or **mapping, or a value otherwise cannot be known without running the program, Bindproof skips that conclusion. The aim is useful, actionable findings with low noise.
Get it → aegisystems.gumroad.com