Pydantic dataclass结合ABC时mypy报classmethod调用参数错误
我有一个基于 Pydantic 的简单 dataclass:
from pydantic.dataclasses import dataclass from abc import ABCMeta, abstractmethod @dataclass class BaseEntity(metaclass=ABCMeta): @classmethod @abstractmethod def from_dict(cls, other: dict): ... @abstractmethod def dict(self): ... @dataclass class UserEntity(BaseEntity): id: Optional[str] name: str email: str avatar: str @classmethod def from_dict(cls, other: dict): return cls( id=other.get("id"), name=other.get("name"), email=other.get("email"), avatar=other.get("avatar"), )
运行 mypy 时出现以下错误:
app/entities/user.py:25: error: Unexpected keyword argument "id" for "UserEntity" [call-arg]
app/entities/user.py:25: error: Unexpected keyword argument "name" for "UserEntity" [call-arg]
app/entities/user.py:25: error: Unexpected keyword argument "email" for "UserEntity" [call-arg]
app/entities/user.py:25: error: Unexpected keyword argument "avatar" for "UserEntity" [call-arg]
代码可以正常运行,但为什么会出现这些错误?这是我哪里写错了,还是 mypy 的 bug?
mypy 版本:
$ mypy --version mypy 1.0.0 (compiled: yes)
原因分析
问题出在同时使用 Pydantic 的 @dataclass 装饰器和 ABCMeta 元类的组合上。Pydantic 的 dataclass 装饰器会动态生成类的构造函数,但 mypy 在处理带有自定义元类的类时,无法正确识别 Pydantic 生成的构造函数参数,因此误判传入的关键字参数不存在。
另外代码中遗漏了 Optional 的导入(from typing import Optional),虽然运行时不会报错,但也会导致额外的类型检查错误,建议补充该导入。
解决方法
方法1:改用 Pydantic BaseModel(推荐)
Pydantic 的 BaseModel 原生支持抽象基类,类型检查兼容性更好,无需额外处理元类:
from pydantic import BaseModel from typing import Optional from abc import ABC, abstractmethod class BaseEntity(ABC, BaseModel): @classmethod @abstractmethod def from_dict(cls, other: dict): ... @abstractmethod def dict(self): ... class UserEntity(BaseEntity): id: Optional[str] name: str email: str avatar: str @classmethod def from_dict(cls, other: dict): return cls(**other)
方法2:调整基类继承方式(保留 dataclass)
如果坚持使用 dataclass,可以让基类继承 ABC(ABC 已封装 ABCMeta),代替直接指定 metaclass=ABCMeta,这样 mypy 能正确识别子类的构造参数:
from pydantic.dataclasses import dataclass from typing import Optional from abc import ABC, abstractmethod @dataclass class BaseEntity(ABC): @classmethod @abstractmethod def from_dict(cls, other: dict): ... @abstractmethod def dict(self): ... @dataclass class UserEntity(BaseEntity): id: Optional[str] name: str email: str avatar: str @classmethod def from_dict(cls, other: dict): return cls( id=other.get("id"), name=other.get("name"), email=other.get("email"), avatar=other.get("avatar"), )
内容的提问来源于stack exchange,提问作者Rodrigo

