You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

编写Mypy插件:将自定义Possibly类型替换为NotRequired[Optional[T]]

实现Possibly[T]等价于NotRequired[Optional[T]]的Mypy插件方案

因为NotRequired是TypedDict专属的元数据标记(不属于常规类型系统),无法通过纯Python类型别名实现,必须通过Mypy插件在TypedDict的分析阶段介入处理。以下是具体实现方向和代码:

核心思路

NotRequired的作用是将TypedDict的字段标记为可选,同时字段的实际类型是Optional[T]。所以插件需要做两件事:

  • 识别TypedDict中使用Possibly[T]的字段
  • 将该字段类型替换为Optional[T],并把字段加入TypedDict的可选键集合

插件实现代码

1. 定义Possibly泛型占位类

# possibly.__init__.py
from typing import Generic, TypeVar

T = TypeVar("T")

class Possibly(Generic[T]):
    pass

2. 编写Mypy插件

# possibly.plugin
from mypy.plugin import Plugin, TypedDictContext
from mypy.nodes import TypedDictType
from mypy.types import Type, UnionType, NoneType, Instance

class PossiblyPlugin(Plugin):
    def get_typed_dict_hook(self) -> callable | None:
        # 拦截TypedDict的分析过程
        return self.process_typed_dict

    def process_typed_dict(self, ctx: TypedDictContext) -> TypedDictType | None:
        td_type = ctx.typed_dict_type
        new_fields = {}
        new_optional_keys = set(td_type.optional_keys)

        # 遍历TypedDict的所有字段
        for field_name, field_type in td_type.items.items():
            # 判断字段类型是否为Possibly[X]
            if (isinstance(field_type, Instance) 
                and field_type.type.fullname == "possibly.Possibly"
                and len(field_type.args) == 1):
                # 提取泛型参数X,构造Optional[X]
                inner_type = field_type.args[0]
                optional_type = UnionType([inner_type, NoneType()])
                new_fields[field_name] = optional_type
                # 将字段标记为可选,对应NotRequired的效果
                new_optional_keys.add(field_name)
            else:
                new_fields[field_name] = field_type

        # 返回修改后的TypedDict类型
        return TypedDictType(
            items=new_fields,
            optional_keys=new_optional_keys,
            total=td_type.total,
            fallback=td_type.fallback,
            metadata=td_type.metadata,
        )

    def get_type_analyze_hook(self, fullname: str) -> callable | None:
        # 限制Possibly只能在TypedDict中使用
        if fullname == "possibly.Possibly":
            return self.validate_possibly_usage

    def validate_possibly_usage(self, ctx) -> Type | None:
        # 非TypedDict场景下使用Possibly则报错
        ctx.api.msg.error(
            "`Possibly[T]`仅能作为TypedDict的字段类型使用",
            ctx.context
        )
        # 返回object作为占位类型,避免后续类型错误
        return ctx.api.named_type("builtins.object")

def plugin(version: str) -> type[Plugin]:
    return PossiblyPlugin

插件配置与使用

  1. 在mypy.ini中启用插件:
[mypy]
plugins = possibly.plugin
  1. 在代码中使用Possibly:
from typing import TypedDict
from possibly import Possibly

class User(TypedDict):
    id: int
    name: Possibly[str]  # 等价于NotRequired[Optional[str]]

关键说明

  • get_typed_dict_hook是Mypy提供的专门处理TypedDict的钩子,能在TypedDict定义被分析时介入修改
  • 替换字段类型为Optional[T](即Union[T, None]),同时将字段加入可选键集合,完美模拟NotRequired[Optional[T]]的效果
  • get_type_analyze_hook用于限制Possibly的使用场景,避免在非TypedDict环境下误用

内容的提问来源于stack exchange,提问作者Jonathan Herrera

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 11:06:35