Skip to main content

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_construct signatures for SQLModel models:
    • treat sqlmodel.Field(...) required/optional correctly
    • accept sqlmodel.Relationship(...) kwargs for table=True models
    • accept common Field(...) alias kwargs (alias / validation_alias) when statically known
  • Improve SQLAlchemy expression typing for class attribute access on table=True models (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=True models (e.g. User.__table__).
  • Relationship comparator typing on relationship attributes (e.g. Team.heroes.any(...), Hero.team.has(...), Team.heroes.contains(...)).
  • Outer-join None propagation in select(A, B).join(B, isouter=True) result tuples.
  • Broaden Session.exec() / AsyncSession.exec() typing to accept SQLAlchemy Executable statements (e.g. text(...)), not just select(...).
  • Optional typed Session.execute() / AsyncSession.execute() for select(...) (opt-in; see typed_execute).
  • Accept SQLAlchemy TypeEngine instances in Field(sa_type=...) (e.g. DateTime(timezone=True), String(50)) without # type: ignore in strict mode.
  • Accept model_config = ConfigDict(...) overrides on SQLModel subclasses in strict mode.
  • Compatible with pydantic.mypy when sqlmodel_mypy.plugin is 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. When false, the plugin uses Any for parameter types (but still enforces required/optional fields).
  • init_forbid_extra (default: false): When true, do not add **kwargs: Any to generated __init__ / model_construct / constructor signatures, so mypy reports unexpected keyword arguments.
  • warn_untyped_fields (default: true): When true, emit error code sqlmodel-field for untyped declarations like x = Field(...) / x = Relationship(...) (use x: T = ... instead).
  • typed_execute (default: false): When true, type SQLModel’s deprecated Session.execute() / AsyncSession.execute() for common select(...) statements by recovering Result[...] generics. This is meant to reduce “type clutter” during migrations; prefer Session.exec() for new code.
  • debug_dataclass_transform (default: false): Advanced/debug-only. When true, 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. untyped x = Field(...)).
  • sqlmodel-plugin-order: plugin ordering errors (e.g. pydantic.mypy enabled before sqlmodel_mypy.plugin). Fix: list sqlmodel_mypy.plugin first (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(...) (infers Mapped[T] from ScalarSelect[T] / ColumnElement[T]) to support patterns like Model._foo = column_property(...) combined with getattr(Model, "_foo") in query builders.
  • 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=True expression 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

sqlmodel_mypy_plugin-0.19.1.tar.gz (72.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sqlmodel_mypy_plugin-0.19.1-py3-none-any.whl (35.1 kB view details)

Uploaded Python 3

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

Hashes for sqlmodel_mypy_plugin-0.19.1.tar.gz
Algorithm Hash digest
SHA256 720c2c7317e17375fce76318928159a5a77ced6c64998d327893671e5a86b09e
MD5 3eb13be9e86da8f7e86240712dff11b8
BLAKE2b-256 699e14dca8c17091665f85f529c25b7a3a7fa33c752a94f9dd44d7c9de5ff5ae

See more details on using hashes here.

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

Hashes for sqlmodel_mypy_plugin-0.19.1-py3-none-any.whl
Algorithm Hash digest
SHA256 34edc8e4de5a03166ddf4178ca774c8fed46e68ac7c07fee52c306c997321158
MD5 cf5603a0aefebfdf269068199b280f5d
BLAKE2b-256 1771574c4a3bbe2fdc722efd9d2120a480e578b37806aa474f6a26c786df3ac2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page