Python条件类型提示:如何实现Result类字段条件必填与类型收窄
问题解决方案
单类双Optional字段的定义无法给类型检查器提供两个字段的关联规则,是静态类型收窄失效的核心原因。目前Python生态(mypy、pyright、ruff等所有主流静态检查工具)原生支持的最优实现是判别联合(Tagged Union),既能满足运行时合法性校验,也能实现你要的分支类型自动收窄。
核心实现思路
不要在单个类上同时定义两个可选字段,而是把Result拆成两种互斥状态的类型联合:
- 成功态:
data字段为真值,error固定为None - 失败/异常态:
error字段为非None的异常对象,data可以是None或者falsy的兜底值(比如你提到的空字符串)
两种状态都强制要求必填对应字段,从根源上禁止两个字段都为空的非法实例。
1. 无第三方依赖的纯typing实现
用dataclass定义两种状态,再通过Union组装成Result泛型:
from typing import Generic, Optional, TypeVar, Union from dataclasses import dataclass Dto = TypeVar("Dto") @dataclass class Ok(Generic[Dto]): data: Dto error: None = None def __post_init__(self): # 运行时校验:成功态data必须为真值 if not self.data: raise ValueError("Success state requires truthy data") @property def is_success(self) -> bool: return True @dataclass class Err(Generic[Dto]): error: Exception data: Optional[Dto] = None @property def is_success(self) -> bool: return False Result = Union[Ok[Dto], Err[Dto]]
2. 适配Pydantic的实现
Pydantic V2原生支持联合类型的校验,配合根校验器可以完全匹配你的规则:
from typing import Generic, Optional, TypeVar, Union from pydantic import BaseModel, root_validator # 保留原有的Dto约束即可 Dto = TypeVar("Dto", bound=BaseModel) class Ok(BaseModel, Generic[Dto]): data: Dto error: None = None @root_validator def check_truthy_data(cls, values): if not values.get("data"): raise ValueError("Success state requires truthy data") return values @property def is_success(self) -> bool: return True class Config: arbitrary_types_allowed = True class Err(BaseModel, Generic[Dto]): error: Exception data: Optional[Dto] = None @property def is_success(self) -> bool: return False class Config: arbitrary_types_allowed = True Result = Union[Ok[Dto], Err[Dto]]
效果验证
两种实现都完全符合你列的约束规则:
- 合法实例:
Ok(data='a')成功态,校验通过Err(error=Exception())纯异常态,校验通过Err(data='', error=Exception())带兜底空数据的异常态,校验通过
- 非法实例:
- 直接构造空
Result()会报参数缺失错误,静态检查阶段就能拦截 - 构造
Ok(data='')会被运行时校验拦截,不允许出现data为falsy且无error的状态
- 直接构造空
- 类型收窄效果:
def handle_result(res: Result[str]): if res.data: # 该分支下静态检查器自动收窄为Ok[str]类型,可100%确定res.error为None print(res.data.upper()) # 不会报None类型错误 else: # 该分支下自动收窄为Err[str]类型,可100%确定res.error为非None的Exception对象 print(res.error.with_traceback(None)) # 不会报None类型错误
注意事项
不要尝试仅通过单个类的根校验器实现该需求——运行时校验只能拦截非法实例,无法给静态检查器提供字段关联信息,linter依然无法完成分支类型收窄。判别联合是目前Python类型系统下唯一能同时满足静态检查、运行时校验、非法实例拦截三个需求的方案。
内容的提问来源于stack exchange,提问作者Tâmer Cuba
相关产品推荐
相关产品推荐

