Mypy plugin for SQLModel (better constructor/Field typing).
Project description
SQLModel Mypy plugin
Mypy plugin that improves type checking for SQLModel models.
Status
Early/experimental. Implemented scope (supported today):
- Generate correct
__init__/model_constructsignatures for SQLModel models:- treat
sqlmodel.Field(...)required/optional correctly - accept
sqlmodel.Relationship(...)kwargs fortable=Truemodels - accept common
Field(...)alias kwargs (alias/validation_alias) when statically known
- treat
- Improve SQLAlchemy expression typing for class attribute access on
table=Truemodels (e.g.User.id,User.name,Team.heroes). - Common SQLAlchemy helpers that operate on expressions / selects (e.g.
.label(...),selectinload(...),execution_options(...)) work without spurious mypy errors. - Expose SQLAlchemy table metadata on
table=Truemodels (e.g.User.__table__). - Relationship comparator typing on relationship attributes (e.g.
Team.heroes.any(...),Hero.team.has(...),Team.heroes.contains(...)). - Outer-join
Nonepropagation inselect(A, B).join(B, isouter=True)result tuples. - Broaden
Session.exec()/AsyncSession.exec()typing to accept SQLAlchemyExecutablestatements (e.g.text(...)), not justselect(...). - Optional typed
Session.execute()/AsyncSession.execute()forselect(...)(opt-in; seetyped_execute). - Accept SQLAlchemy TypeEngine instances in
Field(sa_type=...)(e.g.DateTime(timezone=True),String(50)) without# type: ignorein strict mode. - Accept
model_config = ConfigDict(...)overrides on SQLModel subclasses in strict mode. - Compatible with
pydantic.mypywhensqlmodel_mypy.pluginis listed first.
Install
uv add sqlmodel-mypy-plugin
Install (dev)
Prereqs: Python 3.10+ and uv.
make install
Run the full quality gate suite (same as CI):
make check
Enable in mypy
mypy.ini:
[mypy]
plugins = sqlmodel_mypy.plugin
If you also use pydantic.mypy, sqlmodel_mypy.plugin must be listed first:
[mypy]
plugins = sqlmodel_mypy.plugin, pydantic.mypy
This is not a preference: mypy picks the first plugin that claims a hook. If pydantic.mypy runs first, it
can treat SQLModel classes as plain Pydantic models and you’ll get incorrect typing (e.g. missing __table__,
broken SQLAlchemy expression types, and incorrect/empty constructor signatures).
Plugin configuration
All options are boolean.
Supported options:
init_typed(default:false): Generate__init__/ constructor signatures using the declared field types. Whenfalse, the plugin usesAnyfor parameter types (but still enforces required/optional fields).init_forbid_extra(default:false): Whentrue, do not add**kwargs: Anyto generated__init__/model_construct/ constructor signatures, so mypy reports unexpected keyword arguments.warn_untyped_fields(default:true): Whentrue, emit error codesqlmodel-fieldfor untyped declarations likex = Field(...)/x = Relationship(...)(usex: T = ...instead).typed_execute(default:false): Whentrue, type SQLModel’s deprecatedSession.execute()/AsyncSession.execute()for commonselect(...)statements by recoveringResult[...]generics. This is meant to reduce “type clutter” during migrations; preferSession.exec()for new code.debug_dataclass_transform(default:false): Advanced/debug-only. Whentrue, keep SQLModel’s__dataclass_transform__handling enabled in mypy (useful for debugging plugin interactions; not recommended for normal use).
mypy.ini:
[sqlmodel-mypy]
init_typed = false
init_forbid_extra = false
warn_untyped_fields = true
typed_execute = false
debug_dataclass_transform = false
pyproject.toml:
[tool.sqlmodel-mypy]
init_typed = false
init_forbid_extra = false
warn_untyped_fields = true
typed_execute = false
debug_dataclass_transform = false
Error codes
sqlmodel-field: field-related plugin errors (e.g. untypedx = Field(...)).sqlmodel-plugin-order: plugin ordering errors (e.g.pydantic.mypyenabled beforesqlmodel_mypy.plugin). Fix: listsqlmodel_mypy.pluginfirst (see Enable in mypy).
SQL expression typing
This plugin adjusts class attribute types on table=True SQLModel models to behave like SQLAlchemy
expressions, so you can write queries without col():
from sqlmodel import Field, SQLModel, select
class User(SQLModel, table=True):
id: int = Field(primary_key=True)
name: str = Field()
stmt = select(User).where(User.name.like("%x%"))
You can also use SQLAlchemy table metadata attributes without attr-defined noise:
tbl = User.__table__
getattr(Model, "field") support
If you build query filters dynamically, getattr(Model, "field") with a string literal (or a Final / string
Literal[...] name) is typed the same as direct Model.field access on table=True models:
stmt = select(User).where(getattr(User, "name").like("%x%"))
Example using a Final constant:
from typing import Final
FIELD: Final = "name"
stmt = select(User).where(getattr(User, FIELD).like("%x%"))
Non-literal names (runtime strings) are intentionally left as Any (you’ll still need a cast or a different
pattern).
Relationship comparator typing
Relationship attributes declared via Relationship(...) are typed as SQLAlchemy expressions at class level,
including common relationship comparators used in query filters:
stmt = select(Team).where(Team.heroes.any(Hero.name == "x"))
stmt = select(Hero).where(Hero.team.has(Team.name == "t"))
stmt = select(Team).where(Team.heroes.contains(hero_obj))
Out of scope
- Most SQLAlchemy-only declaration helpers: this plugin doesn’t try to “fix” the full declaration-site typing
for SQLAlchemy ORM APIs that aren’t part of SQLModel’s surface area.
- Exception: best-effort return typing for
column_property(...)(infersMapped[T]fromScalarSelect[T]/ColumnElement[T]) to support patterns likeModel._foo = column_property(...)combined withgetattr(Model, "_foo")in query builders.
- Exception: best-effort return typing for
- Pydantic
@computed_field: not touched by this plugin (it should type-check via normal property typing / Pydantic typing).
Typing strategy (defaults)
- Plugin hooks: SQLModel-specific behavior (constructor signatures,
table=Trueexpression typing, etc.). - SQLAlchemy typing: relied upon for core SQL/ORM typing wherever possible.
- Stub overlays: only for upstream gaps that can’t be addressed cleanly via hooks (prefer upstream fixes first).
Field aliases in constructor kwargs
If you use Field(alias=...) (or validation_alias=... / schema_extra={"validation_alias": ...}), the plugin
adds the alias name as an accepted keyword argument in generated __init__ / model_construct signatures.
This avoids false-positive “unexpected keyword argument” errors when init_forbid_extra=true.
Limitations:
- Only string-literal aliases are recognized (e.g.
Field(alias=\"full_name\")). - Aliases that are not valid Python identifiers (or that collide with other parameter names) are ignored.
- Mypy can’t express “either field name or alias is required”, so aliased required fields may not be reported as missing at type-check time.
Persisted model helpers (opt-in)
Strict typing + SQLModel often means table-model IDs start as None and are populated after flush/commit/refresh
(see SQLModel: Automatic IDs, None, and Refresh).
Mypy can’t infer those runtime effects, so you can end up with repeated assert obj.id is not None.
This package ships small helpers you can import (they do not affect plugin behavior):
from sqlmodel_mypy import has_id, require_id
hero_id: int = require_id(hero) # raises ValueError if hero.id is None
if has_id(hero):
ok_id: int = hero.id
Development
- List available commands:
make help - Lint/format:
make lint/make fmt - Typecheck plugin code:
make typecheck - Run tests:
make test - Run all quality gates:
make check - Update mypy "golden" outputs:
make update-mypy
See AGENTS.md for repo conventions and workflow notes.
Security
See SECURITY.md.
Code of conduct
See CODE_OF_CONDUCT.md.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sqlmodel_mypy_plugin-0.19.1.tar.gz.
File metadata
- Download URL: sqlmodel_mypy_plugin-0.19.1.tar.gz
- Upload date:
- Size: 72.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
720c2c7317e17375fce76318928159a5a77ced6c64998d327893671e5a86b09e
|
|
| MD5 |
3eb13be9e86da8f7e86240712dff11b8
|
|
| BLAKE2b-256 |
699e14dca8c17091665f85f529c25b7a3a7fa33c752a94f9dd44d7c9de5ff5ae
|
File details
Details for the file sqlmodel_mypy_plugin-0.19.1-py3-none-any.whl.
File metadata
- Download URL: sqlmodel_mypy_plugin-0.19.1-py3-none-any.whl
- Upload date:
- Size: 35.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.9.27 {"installer":{"name":"uv","version":"0.9.27","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
34edc8e4de5a03166ddf4178ca774c8fed46e68ac7c07fee52c306c997321158
|
|
| MD5 |
cf5603a0aefebfdf269068199b280f5d
|
|
| BLAKE2b-256 |
1771574c4a3bbe2fdc722efd9d2120a480e578b37806aa474f6a26c786df3ac2
|