SQLAlchemy命令式映射类filter方法的Mypy类型支持问题
问题:SQLAlchemy命令式映射后Mypy类型错误解决
问题背景
用attrs定义User领域类,通过SQLAlchemy的mapper_registry.map_imperatively实现领域层与数据库后端分离(方便独立单元测试),功能运行正常,但调用Query.filter()使用User类属性时,Mypy提示类型错误:filter期望列表达式而非普通Python类型,不想通过# type: ignore规避,寻求正确解决方案。
相关代码
领域类
from attrs import define, field @define(slots=False, kw_only=True) class User: id: int = field(init=False) username: str = field() hashed_password: str = field() is_active: bool = field(default=True) is_superuser: bool = field(default=False)
数据库表定义
import sqlalchemy as sa from sqlalchemy.sql import expression from ..base import mapper_registry user = sa.Table( "users", mapper_registry.metadata, sa.Column("id", sa.Integer, primary_key=True), sa.Column("username", sa.String(155), unique=True, nullable=False), sa.Column("hashed_password", sa.String(200), nullable=False), sa.Column("is_active", sa.Boolean, nullable=False, server_default=expression.true()), sa.Column("is_superuser", sa.Boolean, nullable=False, server_default=expression.false()) )
映射配置
from .base import mapper_registry from .tables import user from project.models import User mapper_registry.map_imperatively(User, user)
查询代码
from typing import List from sqlalchemy import true from sqlalchemy.orm import Session from project.models import User def list_active(session: Session) -> List[User]: return session.query(User).filter(User.is_active == true()).all()
错误信息(中文翻译)
project/queries.py12: error: "filter"的参数1类型不兼容,期望类型为"Union[ColumnElement[bool], _HasClauseElement, SQLCoreOperations[bool], ExpressionElementRole[bool], Callable[[], ColumnElement[bool]], LambdaElement]",实际为"bool" [arg-type]
解决方案
方案一:通过类型检查标记兼容领域类与SQLAlchemy类型
利用TYPE_CHECKING常量区分运行时与类型检查场景,给领域类属性标注SQLAlchemy的InstrumentedAttribute类型,既保持领域类运行时的纯Python特性,又让Mypy识别属性的SQLAlchemy列表达式类型。
修改后的领域类代码:
from attrs import define, field from typing import TYPE_CHECKING # 仅在类型检查时导入SQLAlchemy类型 if TYPE_CHECKING: from sqlalchemy.orm import InstrumentedAttribute @define(slots=False, kw_only=True) class User: id: "InstrumentedAttribute[int]" = field(init=False) username: "InstrumentedAttribute[str]" = field() hashed_password: "InstrumentedAttribute[str]" = field() is_active: "InstrumentedAttribute[bool]" = field(default=True) is_superuser: "InstrumentedAttribute[bool]" = field(default=False)
方案二:直接标注InstrumentedAttribute类型(不区分场景)
如果不需要严格保持领域类的纯Python特性,可以直接给属性标注InstrumentedAttribute类型:
from attrs import define, field from sqlalchemy.orm import InstrumentedAttribute @define(slots=False, kw_only=True) class User: id: InstrumentedAttribute[int] = field(init=False) username: InstrumentedAttribute[str] = field() hashed_password: InstrumentedAttribute[str] = field() is_active: InstrumentedAttribute[bool] = field(default=True) is_superuser: InstrumentedAttribute[bool] = field(default=False)
不推荐方案:直接使用表列对象
虽然可以通过user.c.is_active代替User.is_active来避免类型错误,但这种方式会让查询代码耦合数据库表定义,违背了领域层与后端分离的初衷,因此不推荐。
总结
推荐使用方案一,既能保持领域类的独立性(运行时仍是纯Python类,方便单元测试),又能彻底解决Mypy的类型错误,无需使用# type: ignore。
内容的提问来源于stack exchange,提问作者Danosaurus
相关产品推荐
相关产品推荐

