qusql-py-mysql-type
qusql-py-mysql-type and qusql-mysql-type-plugin let you write MySQL queries
in Python that are type-checked by mypy at static analysis time, with no runtime
surprises.
The plugin is implemented as a native extension (compiled Rust via PyO3) that
mypy loads to evaluate your schema and annotate every execute() call.
Setup
Install the packages (e.g. with uv):
# pyproject.toml
[project]
dependencies = ["qusql-mysql-type"]
[dependency-groups]
dev = ["qusql-mysql-type-plugin", "mypy", "types-mysqlclient"]
[tool.mypy]
plugins = ["qusql_mysql_type_plugin"]
Place your schema in mysql-type-schema.sql in the working directory where
mypy runs (usually the project root):
CREATE TABLE IF NOT EXISTS notes (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
body TEXT,
pinned TINYINT(1) NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Usage
from typing import cast
import MySQLdb
import MySQLdb.cursors
from qusql_mysql_type import execute
conn = MySQLdb.connect(host="127.0.0.1", user="test", passwd="test", db="test")
# cast() is required because MySQLdb stubs leave cursor() return type as Any
c = cast(MySQLdb.cursors.Cursor, conn.cursor())
# mypy infers: list[tuple[int, str, str | None]]
rows = execute(c, "SELECT id, title, body FROM notes ORDER BY id").fetchall()
for note_id, title, body in rows:
print(title, body or "")
What mypy checks
- Column names referenced in
SELECTexist in the table %sargument count matches the query%sargument types match the expected SQL column types- Inferred return type flows into the rest of your code; wrong destructuring patterns are caught at analysis time
- Invalid SQL is a mypy error
The _LIST_ expansion
For IN (...) queries with a variable-length list, use _LIST_:
ids = [1, 2, 3]
rows = execute(c, "SELECT id, title FROM notes WHERE id IN (_LIST_)", ids).fetchall()
_LIST_ is expanded at runtime to the correct number of %s placeholders. If
the list is empty it becomes NULL.