2026, Jan 02 12:01
mypy и SQLAlchemy: почему не ловятся сравнения и что делать
Почему mypy не помечает неверные сравнения столбцов в SQLAlchemy ORM и как это исправить: специализированные типы и перегрузка __eq__ для строгой проверки.
Статическая типизация и SQLAlchemy вполне уживаются, но есть острый угол, который часто сбивает с толку: проверки равенства для столбцов ORM. Можно сравнить целочисленный столбец со строкой, получить вполне корректный объект SQL‑выражения — и mypy не возразит. Если вы ждёте предупреждение Non-overlapping equality check или reportUnnecessaryComparison, «из коробки» его не будет.
Постановка задачи
Настройка повторяет документацию по плагину mypy для SQLAlchemy. В примере целочисленный первичный ключ сравнивается со строкой внутри where, и проверка типов молчит.
from sqlalchemy import Column, Integer, String, select
from sqlalchemy.orm import declarative_base
OrmBase = declarative_base()
class Account(OrmBase):
__tablename__ = "account"
id = Column(Integer, primary_key=True)
label = Column(String)
q = select(
Account.label
).where(
Account.id == "test" # сравнение целочисленного столбца со строкой
)
Проверка типов запускается так:
mypy --strict --config-file mypy.ini test.py
А в конфигурации подключён плагин SQLAlchemy:
[mypy]
plugins = sqlalchemy.ext.mypy.plugin
Почему mypy это не помечает
С точки зрения нативной типизации оператор равенства для столбца ORM возвращает ColumnElement[bool]. Это полноценный, нетривиальный объект выражения, который метод where в ORM принимает без проблем. Иными словами, система типов видит лишь «валидное булево SQL‑выражение», а не «очевидно несовместимое сравнение на равенство». Поскольку Column.__eq__ выдаёт ColumnElement[bool], у проверяющего типов нет оснований сигнализировать Non-overlapping equality check или reportUnnecessaryComparison.
Практическое обходное решение
Если хочется, чтобы типизатор отвергал явно несовместимые сравнения, можно ввести более специализированный тип столбца и перегрузить у него оператор равенства. Идея проста: сравнение с корректным Python‑типом должно давать обычное булево SQL‑выражение, а сравнение с любым другим типом — быть статически ложным. Тогда у проверяющего появляется понятный сигнал.
class IntField(Column[int]):
if TYPE_CHECKING:
@overload
def __eq__(self, other: int) -> ColumnElement[bool]: # type: ignore[override]
...
@overload
def __eq__(self, other: object) -> Literal[False]:
...
class Account(OrmBase):
__tablename__ = "account"
id = IntField(Integer, primary_key=True)
label = Column(String)
С таким подходом сравнение целочисленного id со строкой становится статически ложным, и типизатор сможет вывести это как ошибку. Есть и компромисс: если сравнивать столбец с нелитеральным объединением типов, например когда obj: int | str, выражение вида Account.id == obj тоже будет помечено. Для SQLAlchemy это безопасно на рантайме, но типовая ошибка всё равно появится.
Есть альтернативная сигнатура: во втором перегрузе вернуть ColumnElement[Literal[False]]. Это снимает ошибки в общем случае, но и сравнение со строкой перестанет подсвечиваться.
Эти идеи также подталкивают к написанию собственного плагина, который трактует ColumnElement[Literal[False]] как ошибку и, при желании, позволяет обойтись без --strict-equality. Этим путём стоит идти, если проекту нужны более жёсткие гарантии.
Почему это важно
Перегрузки операторов в SQLAlchemy предназначены для построения деревьев SQL‑выражений, а не для проверки совместимости значений на уровне Python. Это удобно при конструировании запросов, но скрывает очевидные несостыковки от статического анализа. Если команда рассчитывает на mypy для отлова неверных сравнений, такая щель позволяет некоторым багам пройти код‑ревью незамеченными.
Вывод
Ожидать, что mypy пометит не пересекающиеся сравнения на равенство для столбцов ORM, нельзя, опираясь только на нативную типизацию: Column.__eq__ намеренно возвращает валидный ColumnElement[bool]. Если важна реакция типизатора, специализируйте столбцы и перегрузите __eq__, чтобы несовместимые сравнения становились статически ложными. Учитывайте ограничения при объединениях типов и подумайте о небольшом плагине вокруг ColumnElement[Literal[False]], если нужна более системная проверка. Понимание того, где типы SQL‑выражений расходятся с типами значений в Python, помогает решить, когда затянуть гайки, а когда довериться поведению на рантайме.